feat: public-hoist-pattern

close #2628
PR #2631
This commit is contained in:
Zoltan Kochan
2020-06-16 00:50:09 +03:00
committed by GitHub
parent 6808c43faf
commit 71a8c8ce38
41 changed files with 457 additions and 216 deletions

View File

@@ -0,0 +1,7 @@
---
"@pnpm/read-projects-context": major
---
`hoistedDependencies` is returned instead of `hoistedAliases`.
`currentPublicHoistPattern` is returned instead of `shamefullyHoist`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/types": minor
---
Added a new type: HoistedDependencies.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/matcher": patch
---
When no patterns are passed in, create a matcher that always returns `false`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/hoist": major
---
Breaking changes in the API.

View File

@@ -0,0 +1,7 @@
---
"@pnpm/modules-cleaner": major
---
Replaced `hoistedAliases` with `hoistedDependencies`.
Added `publicHoistedModulesDir` option.

View File

@@ -0,0 +1,6 @@
---
"@pnpm/config": minor
"@pnpm/plugin-commands-installation": minor
---
Added a new setting: `public-hoist-pattern`. This setting can be overwritten by `--[no-]shamefully-hoist`. The default value of `public-hoist-pattern` is `types/*`.

View File

@@ -0,0 +1,5 @@
---
"supi": minor
---
`shamefullyHoist` replaced by `publicHoistPattern` and `forcePublicHoistPattern`.

View File

@@ -0,0 +1,7 @@
---
"@pnpm/headless": major
---
`hoistedAliases` replaced with `hoistedDependencies`.
`shamefullyHoist` replaced with `publicHoistPattern`.

View File

@@ -0,0 +1,9 @@
---
"@pnpm/get-context": major
---
`hoistedAliases` replaced with `hoistedDependencies`.
`shamefullyHoist` replaced with `publicHoistPattern`.
`forceShamefullyHoist` replaced with `forcePublicHoistPattern`.

View File

@@ -0,0 +1,9 @@
---
"@pnpm/headless": major
"@pnpm/modules-yaml": major
"supi": minor
---
Breaking changes to the `node_modules/.modules.yaml` file:
* `hoistedAliases` replaced with `hoistedDependencies`.
* `shamefullyHoist` replaced with `publicHoistPattern`.

View File

@@ -99,6 +99,7 @@ export interface Config {
pnpmfile: string,
packageImportMethod?: 'auto' | 'hardlink' | 'copy' | 'clone',
hoistPattern?: string[],
publicHoistPattern?: string[],
useStoreServer?: boolean,
useRunningStoreServer?: boolean,
workspaceConcurrency: number,

View File

@@ -60,6 +60,7 @@ export const types = Object.assign({
'prefer-frozen-shrinkwrap': Boolean,
'prefer-offline': Boolean,
'production': [null, true],
'public-hoist-pattern': Array,
'publish-branch': String,
'reporter': String,
'save-peer': Boolean,
@@ -146,10 +147,10 @@ export default async (
'link-workspace-packages': true,
'package-lock': npmDefaults['package-lock'],
'pending': false,
'public-hoist-pattern': ['@types/*'],
'registry': npmDefaults.registry,
'save-peer': false,
'save-workspace-protocol': true,
'shamefully-hoist': false,
'shared-workspace-lockfile': true,
'shared-workspace-shrinkwrap': true,
'shrinkwrap': npmDefaults.shrinkwrap,
@@ -311,6 +312,14 @@ export default async (
if (pnpmConfig['hoist'] === false) {
delete pnpmConfig.hoistPattern
}
switch (pnpmConfig.shamefullyHoist) {
case false:
delete pnpmConfig.publicHoistPattern
break
case true:
pnpmConfig.publicHoistPattern = ['*']
break
}
if (typeof pnpmConfig['color'] === 'boolean') {
switch (pnpmConfig['color']) {
case true:

View File

@@ -9,6 +9,7 @@ import {
import readProjectsContext from '@pnpm/read-projects-context'
import {
DEPENDENCIES_FIELDS,
HoistedDependencies,
ProjectManifest,
ReadPackageHook,
Registries,
@@ -27,7 +28,7 @@ export interface PnpmContext<T> {
existsCurrentLockfile: boolean,
existsWantedLockfile: boolean,
extraBinPaths: string[],
hoistedAliases: {[depPath: string]: string[]}
hoistedDependencies: HoistedDependencies,
include: IncludedDependencies,
modulesFile: Modules | null,
pendingBuilds: string[],
@@ -38,9 +39,9 @@ export interface PnpmContext<T> {
rootModulesDir: string,
hoistPattern: string[] | undefined,
hoistedModulesDir: string,
publicHoistPattern: string[] | undefined,
lockfileDir: string,
virtualStoreDir: string,
shamefullyHoist: boolean,
skipped: Set<string>,
storeDir: string,
wantedLockfile: Lockfile,
@@ -75,8 +76,8 @@ export default async function getContext<T> (
hoistPattern?: string[] | undefined,
forceHoistPattern?: boolean,
shamefullyHoist?: boolean,
forceShamefullyHoist?: boolean,
publicHoistPattern?: string[] | undefined,
forcePublicHoistPattern?: boolean,
}
): Promise<PnpmContext<T>> {
const modulesDir = opts.modulesDir ?? 'node_modules'
@@ -86,6 +87,7 @@ export default async function getContext<T> (
if (importersContext.modules) {
const { purged } = await validateModules(importersContext.modules, importersContext.projects, {
currentHoistPattern: importersContext.currentHoistPattern,
currentPublicHoistPattern: importersContext.currentPublicHoistPattern,
forceNewModules: opts.forceNewModules === true,
include: opts.include,
lockfileDir: opts.lockfileDir,
@@ -97,8 +99,8 @@ export default async function getContext<T> (
forceHoistPattern: opts.forceHoistPattern,
hoistPattern: opts.hoistPattern,
forceShamefullyHoist: opts.forceShamefullyHoist,
shamefullyHoist: opts.shamefullyHoist,
forcePublicHoistPattern: opts.forcePublicHoistPattern,
publicHoistPattern: opts.publicHoistPattern,
})
if (purged) {
importersContext = await readProjectsContext(projects, {
@@ -126,28 +128,26 @@ export default async function getContext<T> (
const extraBinPaths = [
...opts.extraBinPaths || [],
]
const shamefullyHoist = Boolean(typeof importersContext.shamefullyHoist === 'undefined' ? opts.shamefullyHoist : importersContext.shamefullyHoist)
if (opts.hoistPattern && !shamefullyHoist) {
extraBinPaths.unshift(path.join(virtualStoreDir, 'node_modules/.bin'))
const hoistedModulesDir = path.join(virtualStoreDir, 'node_modules')
if (opts.hoistPattern?.length) {
extraBinPaths.unshift(path.join(hoistedModulesDir, '.bin'))
}
const hoistedModulesDir = shamefullyHoist
? importersContext.rootModulesDir : path.join(virtualStoreDir, 'node_modules')
const ctx: PnpmContext<T> = {
extraBinPaths,
hoistedAliases: importersContext.hoistedAliases,
hoistedDependencies: importersContext.hoistedDependencies,
hoistedModulesDir,
hoistPattern: opts.hoistPattern,
hoistPattern: importersContext.currentHoistPattern ?? opts.hoistPattern,
include: opts.include || importersContext.include,
lockfileDir: opts.lockfileDir,
modulesFile: importersContext.modules,
pendingBuilds: importersContext.pendingBuilds,
projects: importersContext.projects,
publicHoistPattern: importersContext.currentPublicHoistPattern ?? opts.publicHoistPattern,
registries: {
...opts.registries,
...importersContext.registries,
},
rootModulesDir: importersContext.rootModulesDir,
shamefullyHoist,
skipped: importersContext.skipped,
storeDir: opts.storeDir,
virtualStoreDir,
@@ -174,6 +174,7 @@ async function validateModules (
}>,
opts: {
currentHoistPattern?: string[],
currentPublicHoistPattern?: string[],
forceNewModules: boolean,
include?: IncludedDependencies,
lockfileDir: string,
@@ -185,44 +186,30 @@ async function validateModules (
hoistPattern?: string[] | undefined,
forceHoistPattern?: boolean,
shamefullyHoist?: boolean | undefined,
forceShamefullyHoist?: boolean,
publicHoistPattern?: string[] | undefined,
forcePublicHoistPattern?: boolean,
}
): Promise<{ purged: boolean }> {
const rootProject = projects.find(({ id }) => id === '.')
if (opts.forceShamefullyHoist && modules.shamefullyHoist !== opts.shamefullyHoist) {
if (opts.forcePublicHoistPattern && !R.equals(modules.publicHoistPattern, opts.publicHoistPattern)) {
if (opts.forceNewModules && rootProject) {
await purgeModulesDirsOfImporter(rootProject)
return { purged: true }
}
if (modules.shamefullyHoist) {
throw new PnpmError(
'SHAMEFULLY_HOIST_WANTED',
'This modules directory was created using the --shamefully-hoist option.'
+ ' You must add that option, or else run "pnpm install" to recreate the modules directory.'
)
}
throw new PnpmError(
'SHAMEFULLY_HOIST_NOT_WANTED',
'This modules directory was created without the --shamefully-hoist option.'
+ ' You must remove that option, or else "pnpm install" to recreate the modules directory.'
'PUBLIC_HOIST_PATTERN_DIFF',
'This modules directory was created using a different public-hoist-pattern value.'
+ ' Run "pnpm install" to recreate the modules directory.'
)
}
let purged = false
if (opts.forceHoistPattern && rootProject) {
try {
if (!R.equals(opts.currentHoistPattern, (opts.hoistPattern || undefined))) {
if (opts.currentHoistPattern) {
throw new PnpmError(
'HOISTING_WANTED',
'This modules directory was created using the --hoist-pattern option.'
+ ' You must add this option, or else add the --force option to recreate the modules directory.'
)
}
throw new PnpmError(
'HOISTING_NOT_WANTED',
'This modules directory was created without the --hoist-pattern option.'
+ ' You must remove that option, or else run "pnpm install" to recreate the modules directory.'
'HOIST_PATTERN_DIFF',
'This modules directory was created using a different hoist-pattern value.'
+ ' Run "pnpm install" to recreate the modules directory.'
)
}
} catch (err) {
@@ -297,7 +284,7 @@ export interface PnpmSingleContext {
existsCurrentLockfile: boolean,
existsWantedLockfile: boolean,
extraBinPaths: string[],
hoistedAliases: {[depPath: string]: string[]},
hoistedDependencies: HoistedDependencies,
hoistedModulesDir: string,
hoistPattern: string[] | undefined,
manifest: ProjectManifest,
@@ -307,11 +294,11 @@ export interface PnpmSingleContext {
include: IncludedDependencies,
modulesFile: Modules | null,
pendingBuilds: string[],
publicHoistPattern: string[] | undefined,
registries: Registries,
rootModulesDir: string,
lockfileDir: string,
virtualStoreDir: string,
shamefullyHoist: boolean,
skipped: Set<string>,
storeDir: string,
wantedLockfile: Lockfile,
@@ -339,20 +326,20 @@ export async function getContextForSingleImporter (
hoistPattern?: string[] | undefined,
forceHoistPattern?: boolean,
shamefullyHoist?: boolean,
forceShamefullyHoist?: boolean,
publicHoistPattern?: string[] | undefined,
forcePublicHoistPattern?: boolean,
},
alreadyPurged: boolean = false
): Promise<PnpmSingleContext> {
const {
currentHoistPattern,
hoistedAliases,
currentPublicHoistPattern,
hoistedDependencies,
projects,
include,
modules,
pendingBuilds,
registries,
shamefullyHoist,
skipped,
rootModulesDir,
} = await readProjectsContext(
@@ -377,6 +364,7 @@ export async function getContextForSingleImporter (
if (modules && !alreadyPurged) {
const { purged } = await validateModules(modules, projects, {
currentHoistPattern,
currentPublicHoistPattern,
forceNewModules: opts.forceNewModules === true,
include: opts.include,
lockfileDir: opts.lockfileDir,
@@ -388,8 +376,8 @@ export async function getContextForSingleImporter (
forceHoistPattern: opts.forceHoistPattern,
hoistPattern: opts.hoistPattern,
forceShamefullyHoist: opts.forceShamefullyHoist,
shamefullyHoist: opts.shamefullyHoist,
forcePublicHoistPattern: opts.forcePublicHoistPattern,
publicHoistPattern: opts.publicHoistPattern,
})
if (purged) {
return getContextForSingleImporter(manifest, opts, true)
@@ -400,17 +388,15 @@ export async function getContextForSingleImporter (
const extraBinPaths = [
...opts.extraBinPaths || [],
]
const sHoist = Boolean(typeof shamefullyHoist === 'undefined' ? opts.shamefullyHoist : shamefullyHoist)
if (opts.hoistPattern && !sHoist) {
extraBinPaths.unshift(path.join(virtualStoreDir, 'node_modules/.bin'))
const hoistedModulesDir = path.join(virtualStoreDir, 'node_modules')
if (opts.hoistPattern?.length) {
extraBinPaths.unshift(path.join(hoistedModulesDir, '.bin'))
}
const hoistedModulesDir = sHoist
? rootModulesDir : path.join(virtualStoreDir, 'node_modules')
const ctx: PnpmSingleContext = {
extraBinPaths,
hoistedAliases,
hoistedDependencies,
hoistedModulesDir,
hoistPattern: opts.hoistPattern,
hoistPattern: currentHoistPattern ?? opts.hoistPattern,
importerId,
include: opts.include || include,
lockfileDir: opts.lockfileDir,
@@ -419,12 +405,12 @@ export async function getContextForSingleImporter (
modulesFile: modules,
pendingBuilds,
prefix: opts.dir,
publicHoistPattern: currentPublicHoistPattern ?? opts.publicHoistPattern,
registries: {
...opts.registries,
...registries,
},
rootModulesDir,
shamefullyHoist: sHoist,
skipped,
storeDir,
virtualStoreDir,

View File

@@ -83,7 +83,6 @@
"@pnpm/link-bins": "workspace:5.3.4",
"@pnpm/lockfile-file": "workspace:3.0.9",
"@pnpm/lockfile-utils": "workspace:2.0.13",
"@pnpm/matcher": "workspace:1.0.2",
"@pnpm/modules-cleaner": "workspace:9.0.2",
"@pnpm/modules-yaml": "workspace:7.0.0",
"@pnpm/package-requester": "workspace:12.0.4",

View File

@@ -37,7 +37,6 @@ import logger, {
LogBase,
streamParser,
} from '@pnpm/logger'
import matcher from '@pnpm/matcher'
import { prune } from '@pnpm/modules-cleaner'
import {
IncludedDependencies,
@@ -51,7 +50,7 @@ import {
StoreController,
} from '@pnpm/store-controller-types'
import symlinkDependency, { symlinkDirectRootDependency } from '@pnpm/symlink-dependency'
import { DependencyManifest, ProjectManifest, Registries } from '@pnpm/types'
import { DependencyManifest, HoistedDependencies, ProjectManifest, Registries } from '@pnpm/types'
import dp = require('dependency-path')
import fs = require('mz/fs')
import pLimit = require('p-limit')
@@ -84,12 +83,12 @@ export interface HeadlessOptions {
pruneDirectDependencies?: boolean,
rootDir: string,
}>,
hoistedAliases: {[depPath: string]: string[]}
hoistedDependencies: HoistedDependencies,
hoistPattern?: string[],
publicHoistPattern?: string[],
lockfileDir: string,
modulesDir?: string,
virtualStoreDir?: string,
shamefullyHoist: boolean,
storeController: StoreController,
sideEffectsCacheRead: boolean,
sideEffectsCacheWrite: boolean,
@@ -128,8 +127,8 @@ export default async (opts: HeadlessOptions) => {
const rootModulesDir = await realpathMissing(path.join(lockfileDir, relativeModulesDir))
const virtualStoreDir = pathAbsolute(opts.virtualStoreDir ?? path.join(relativeModulesDir, '.pnpm'), lockfileDir)
const currentLockfile = opts.currentLockfile || await readCurrentLockfile(virtualStoreDir, { ignoreIncompatible: false })
const hoistedModulesDir = opts.shamefullyHoist
? rootModulesDir : path.join(virtualStoreDir, 'node_modules')
const hoistedModulesDir = path.join(virtualStoreDir, 'node_modules')
const publicHoistedModulesDir = rootModulesDir
for (const { id, manifest, rootDir } of opts.projects) {
if (!satisfiesPackageManifest(wantedLockfile, manifest, id)) {
@@ -162,11 +161,12 @@ export default async (opts: HeadlessOptions) => {
{
currentLockfile,
dryRun: false,
hoistedAliases: opts.hoistedAliases,
hoistedDependencies: opts.hoistedDependencies,
hoistedModulesDir: opts.hoistPattern && hoistedModulesDir || undefined,
include: opts.include,
lockfileDir,
pruneStore: opts.pruneStore,
publicHoistedModulesDir: opts.publicHoistPattern && publicHoistedModulesDir || undefined,
registries: opts.registries,
skipped,
storeController: opts.storeController,
@@ -240,18 +240,20 @@ export default async (opts: HeadlessOptions) => {
})
}
const rootImporterWithFlatModules = opts.hoistPattern && opts.projects.find(({ id }) => id === '.')
let newHoistedAliases!: {[depPath: string]: string[]}
const rootImporterWithFlatModules = (opts.hoistPattern || opts.publicHoistPattern) && opts.projects.find(({ id }) => id === '.')
let newHoistedDependencies!: HoistedDependencies
if (rootImporterWithFlatModules) {
newHoistedAliases = await hoist(matcher(opts.hoistPattern!), {
newHoistedDependencies = await hoist({
lockfile: filteredLockfile,
lockfileDir,
modulesDir: hoistedModulesDir,
registries: opts.registries,
privateHoistedModulesDir: hoistedModulesDir,
privateHoistPattern: opts.hoistPattern ?? [],
publicHoistedModulesDir,
publicHoistPattern: opts.publicHoistPattern ?? [],
virtualStoreDir,
})
} else {
newHoistedAliases = {}
newHoistedDependencies = {}
}
await Promise.all(opts.projects.map(async ({ rootDir, id, manifest, modulesDir }) => {
@@ -302,7 +304,7 @@ export default async (opts: HeadlessOptions) => {
})
}
const extraBinPaths = [...opts.extraBinPaths || []]
if (opts.hoistPattern && !opts.shamefullyHoist) {
if (opts.hoistPattern) {
extraBinPaths.unshift(path.join(virtualStoreDir, 'node_modules/.bin'))
}
await buildModules(graph, Array.from(directNodes), {
@@ -327,14 +329,14 @@ export default async (opts: HeadlessOptions) => {
}
await writeCurrentLockfile(virtualStoreDir, filteredLockfile)
await writeModulesYaml(rootModulesDir, {
hoistedAliases: newHoistedAliases,
hoistedDependencies: newHoistedDependencies,
hoistPattern: opts.hoistPattern,
included: opts.include,
layoutVersion: LAYOUT_VERSION,
packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`,
pendingBuilds: opts.pendingBuilds,
publicHoistPattern: opts.publicHoistPattern,
registries: opts.registries,
shamefullyHoist: opts.shamefullyHoist || false,
skipped: Array.from(skipped),
storeDir: opts.storeDir,
virtualStoreDir,

View File

@@ -532,17 +532,17 @@ test('installing with hoistPattern=*', async (t) => {
const modules = await project.readModulesManifest()
t.deepEqual(modules!.hoistedAliases['/balanced-match/1.0.0'], ['balanced-match'], 'hoisted field populated in .modules.yaml')
t.deepEqual(modules!.hoistedDependencies['/balanced-match/1.0.0'], { 'balanced-match': 'private' }, 'hoisted field populated in .modules.yaml')
t.end()
})
test('installing with hoistPattern=* and shamefullyHoist=true', async (t) => {
test('installing with publicHoistPattern=*', async (t) => {
const prefix = path.join(fixtures, 'simple-shamefully-flatten')
await rimraf(path.join(prefix, 'node_modules'))
const reporter = sinon.spy()
await headless(await testDefaults({ lockfileDir: prefix, reporter, hoistPattern: '*', shamefullyHoist: true }))
await headless(await testDefaults({ lockfileDir: prefix, reporter, publicHoistPattern: '*' }))
const project = assertProject(t, prefix)
t.ok(project.requireModule('is-positive'), 'prod dep installed')
@@ -591,7 +591,7 @@ test('installing with hoistPattern=* and shamefullyHoist=true', async (t) => {
const modules = await project.readModulesManifest()
t.deepEqual(modules!.hoistedAliases['/balanced-match/1.0.0'], ['balanced-match'], 'hoisted field populated in .modules.yaml')
t.deepEqual(modules!.hoistedDependencies['/balanced-match/1.0.0'], { 'balanced-match': 'public' }, 'hoisted field populated in .modules.yaml')
t.end()
})

View File

@@ -39,9 +39,6 @@
{
"path": "../lockfile-utils"
},
{
"path": "../matcher"
},
{
"path": "../modules-cleaner"
},

View File

@@ -44,6 +44,7 @@
"@pnpm/lockfile-types": "workspace:2.0.1",
"@pnpm/lockfile-utils": "workspace:2.0.13",
"@pnpm/lockfile-walker": "workspace:3.0.1",
"@pnpm/matcher": "workspace:^1.0.2",
"@pnpm/pkgid-to-filename": "3.0.0",
"@pnpm/symlink-dependency": "workspace:3.0.6",
"@pnpm/types": "workspace:6.0.0",

View File

@@ -6,20 +6,22 @@ import {
} from '@pnpm/lockfile-utils'
import lockfileWalker, { LockfileWalkerStep } from '@pnpm/lockfile-walker'
import logger from '@pnpm/logger'
import matcher from '@pnpm/matcher'
import pkgIdToFilename from '@pnpm/pkgid-to-filename'
import symlinkDependency from '@pnpm/symlink-dependency'
import { Registries } from '@pnpm/types'
import { HoistedDependencies } from '@pnpm/types'
import * as dp from 'dependency-path'
import path = require('path')
import R = require('ramda')
export default async function hoistByLockfile (
match: (dependencyName: string) => boolean,
opts: {
lockfile: Lockfile,
lockfileDir: string,
modulesDir: string,
registries: Registries,
privateHoistPattern: string[],
privateHoistedModulesDir: string,
publicHoistPattern: string[],
publicHoistedModulesDir: string,
virtualStoreDir: string,
}
) {
@@ -40,56 +42,72 @@ export default async function hoistByLockfile (
}, {}),
depPath: '',
depth: -1,
location: '',
},
...await getDependencies(
step,
0,
{
lockfileDir: opts.lockfileDir,
registries: opts.registries,
virtualStoreDir: opts.virtualStoreDir,
}
),
...await getDependencies(step, 0),
]
const aliasesByDependencyPath = await hoistGraph(deps, opts.lockfile.importers['.']?.specifiers ?? {}, {
dryRun: false,
match,
modulesDir: opts.modulesDir,
const getAliasHoistType = createGetAliasHoistType(opts.publicHoistPattern, opts.privateHoistPattern)
const hoistedDependencies = await hoistGraph(deps, opts.lockfile.importers['.']?.specifiers ?? {}, {
getAliasHoistType,
})
const bin = path.join(opts.modulesDir, '.bin')
await symlinkHoistedDependencies(hoistedDependencies, {
lockfile: opts.lockfile,
lockfileDir: opts.lockfileDir,
privateHoistedModulesDir: opts.privateHoistedModulesDir,
publicHoistedModulesDir: opts.publicHoistedModulesDir,
virtualStoreDir: opts.virtualStoreDir,
})
// Here we only link the bins of the privately hoisted modules.
// The bins of the publicly hoisted modules will be linked together with
// the bins of the project's direct dependencies.
// This is possible because the publicly hoisted modules
// are in the same directory as the regular dependencies.
await linkAllBins(opts.privateHoistedModulesDir)
return hoistedDependencies
}
type GetAliasHoistType = (alias: string) => 'private' | 'public' | false
function createGetAliasHoistType (
publicHoistPattern: string[],
privateHoistPattern: string[]
): GetAliasHoistType {
const publicMatcher = matcher(publicHoistPattern)
const privateMatcher = matcher(privateHoistPattern)
return (alias: string) => {
if (publicMatcher(alias)) return 'public'
if (privateMatcher(alias)) return 'private'
return false
}
}
async function linkAllBins (modulesDir: string) {
const bin = path.join(modulesDir, '.bin')
const warn: WarnFunction = (message, code) => {
if (code === 'BINARIES_CONFLICT') return
logger.warn({ message, prefix: path.join(opts.modulesDir, '../..') })
logger.warn({ message, prefix: path.join(modulesDir, '../..') })
}
try {
await linkBins(opts.modulesDir, bin, { allowExoticManifests: true, warn })
await linkBins(modulesDir, bin, { allowExoticManifests: true, warn })
} catch (err) {
// Some packages generate their commands with lifecycle hooks.
// At this stage, such commands are not generated yet.
// For now, we don't hoist such generated commands.
// Related issue: https://github.com/pnpm/pnpm/issues/2071
}
return aliasesByDependencyPath
}
async function getDependencies (
step: LockfileWalkerStep,
depth: number,
opts: {
registries: Registries,
lockfileDir: string,
virtualStoreDir: string,
}
depth: number
): Promise<Dependency[]> {
const deps: Dependency[] = []
const nextSteps: LockfileWalkerStep[] = []
for (const { pkgSnapshot, depPath, next } of step.dependencies) {
const pkgName = nameVerFromPkgSnapshot(depPath, pkgSnapshot).name
const modules = path.join(opts.virtualStoreDir, pkgIdToFilename(depPath, opts.lockfileDir), 'node_modules')
const allDeps = {
...pkgSnapshot.dependencies,
...pkgSnapshot.optionalDependencies,
@@ -101,7 +119,6 @@ async function getDependencies (
}, {}),
depPath,
depth,
location: path.join(modules, pkgName),
})
nextSteps.push(next())
@@ -115,13 +132,12 @@ async function getDependencies (
return (
await Promise.all(
nextSteps.map((nextStep) => getDependencies(nextStep, depth + 1, opts))
nextSteps.map((nextStep) => getDependencies(nextStep, depth + 1))
)
).reduce((acc, deps) => [...acc, ...deps], deps)
}
export interface Dependency {
location: string,
children: {[alias: string]: string},
depPath: string,
depth: number,
@@ -131,50 +147,60 @@ async function hoistGraph (
depNodes: Dependency[],
currentSpecifiers: {[alias: string]: string},
opts: {
match: (dependencyName: string) => boolean,
modulesDir: string,
dryRun: boolean,
getAliasHoistType: GetAliasHoistType,
}
): Promise<{[alias: string]: string[]}> {
): Promise<HoistedDependencies> {
const hoistedAliases = new Set(R.keys(currentSpecifiers))
const aliasesByDependencyPath: {[depPath: string]: string[]} = {}
const hoistedDependencies: HoistedDependencies = {}
await Promise.all(depNodes
depNodes
// sort by depth and then alphabetically
.sort((a, b) => {
const depthDiff = a.depth - b.depth
return depthDiff === 0 ? a.depPath.localeCompare(b.depPath) : depthDiff
})
// build the alias map and the id map
.map((depNode) => {
.forEach((depNode) => {
for (const childAlias of Object.keys(depNode.children)) {
if (!opts.match(childAlias)) continue
const hoist = opts.getAliasHoistType(childAlias)
if (!hoist) continue
// if this alias has already been taken, skip it
if (hoistedAliases.has(childAlias)) {
continue
}
hoistedAliases.add(childAlias)
const childPath = depNode.children[childAlias]
if (!aliasesByDependencyPath[childPath]) {
aliasesByDependencyPath[childPath] = []
if (!hoistedDependencies[childPath]) {
hoistedDependencies[childPath] = {}
}
aliasesByDependencyPath[childPath].push(childAlias)
hoistedDependencies[childPath][childAlias] = hoist
}
return depNode
})
.map(async (depNode) => {
const pkgAliases = aliasesByDependencyPath[depNode.depPath]
if (!pkgAliases) {
return
}
// TODO when putting logs back in for hoisted packages, you've to put back the condition inside the map,
// TODO look how it is done in linkPackages
if (!opts.dryRun) {
await Promise.all(pkgAliases.map(async (pkgAlias) => {
await symlinkDependency(depNode.location, opts.modulesDir, pkgAlias)
return hoistedDependencies
}
async function symlinkHoistedDependencies (
hoistedDependencies: HoistedDependencies,
opts: {
lockfile: Lockfile,
lockfileDir: string,
privateHoistedModulesDir: string,
publicHoistedModulesDir: string,
virtualStoreDir: string,
}
) {
await Promise.all(
Object.entries(hoistedDependencies)
.map(async ([depPath, pkgAliases]) => {
const pkgName = nameVerFromPkgSnapshot(depPath, opts.lockfile.packages![depPath]).name
const modules = path.join(opts.virtualStoreDir, pkgIdToFilename(depPath, opts.lockfileDir), 'node_modules')
const depLocation = path.join(modules, pkgName)
await Promise.all(Object.entries(pkgAliases).map(async ([pkgAlias, hoistType]) => {
const targetDir = hoistType === 'public'
? opts.publicHoistedModulesDir : opts.privateHoistedModulesDir
await symlinkDependency(depLocation, targetDir, pkgAlias)
}))
}
}))
return aliasesByDependencyPath
))
}

View File

@@ -24,6 +24,9 @@
{
"path": "../lockfile-walker"
},
{
"path": "../matcher"
},
{
"path": "../symlink-dependency"
},

View File

@@ -2,7 +2,10 @@ import escapeStringRegexp = require('escape-string-regexp')
export default function matcher (patterns: string[] | string) {
if (typeof patterns === 'string') return matcherFromPattern(patterns)
if (patterns.length === 0) return matcherFromPattern(patterns[0])
switch (patterns.length) {
case 0: return () => false
case 1: return matcherFromPattern(patterns[0])
}
const matchArr = patterns.map(matcherFromPattern)
return (input: string) => matchArr.some((match) => match(input))
}

View File

@@ -16,6 +16,7 @@ import { StoreController } from '@pnpm/store-controller-types'
import {
DependenciesField,
DEPENDENCIES_FIELDS,
HoistedDependencies,
Registries,
} from '@pnpm/types'
import rimraf = require('@zkochan/rimraf')
@@ -35,8 +36,9 @@ export default async function prune (
opts: {
dryRun?: boolean,
include: { [dependenciesField in DependenciesField]: boolean },
hoistedAliases: {[depPath: string]: string[]},
hoistedDependencies: HoistedDependencies,
hoistedModulesDir?: string,
publicHoistedModulesDir?: string,
wantedLockfile: Lockfile,
currentLockfile: Lockfile,
pruneStore?: boolean,
@@ -114,13 +116,18 @@ export default async function prune (
if (!opts.dryRun) {
if (orphanDepPaths.length) {
if (opts.currentLockfile.packages && opts.hoistedModulesDir) {
const modulesDir = opts.hoistedModulesDir
if (
opts.currentLockfile.packages &&
opts.hoistedModulesDir &&
opts.publicHoistedModulesDir
) {
const binsDir = path.join(opts.hoistedModulesDir, '.bin')
const prefix = path.join(opts.virtualStoreDir, '../..')
await Promise.all(orphanDepPaths.map(async (orphanDepPath) => {
if (opts.hoistedAliases[orphanDepPath]) {
await Promise.all(opts.hoistedAliases[orphanDepPath].map((alias) => {
if (opts.hoistedDependencies[orphanDepPath]) {
await Promise.all(Object.entries(opts.hoistedDependencies[orphanDepPath]).map(([alias, hoistType]) => {
const modulesDir = hoistType === 'public'
? opts.publicHoistedModulesDir! : opts.hoistedModulesDir!
return removeDirectDependency({
name: alias,
}, {
@@ -131,7 +138,7 @@ export default async function prune (
})
}))
}
delete opts.hoistedAliases[orphanDepPath]
delete opts.hoistedDependencies[orphanDepPath]
}))
}

View File

@@ -1,4 +1,4 @@
import { DependenciesField, Registries } from '@pnpm/types'
import { DependenciesField, HoistedDependencies, Registries } from '@pnpm/types'
import isWindows = require('is-windows')
import path = require('path')
import readYamlFile from 'read-yaml-file'
@@ -13,14 +13,16 @@ export type IncludedDependencies = {
}
export interface Modules {
hoistedAliases: {[depPath: string]: string[]}
hoistPattern?: string[]
hoistedAliases?: {[depPath: string]: string[]}, // for backward compatibility
hoistedDependencies: HoistedDependencies,
hoistPattern?: string[],
included: IncludedDependencies,
layoutVersion: number,
packageManager: string,
pendingBuilds: string[],
registries?: Registries, // nullable for backward compatibility
shamefullyHoist: boolean,
shamefullyHoist?: boolean, // for backward compatibility
publicHoistPattern?: string[]
skipped: string[],
storeDir: string,
virtualStoreDir: string,
@@ -28,23 +30,58 @@ export interface Modules {
export async function read (modulesDir: string): Promise<Modules | null> {
const modulesYamlPath = path.join(modulesDir, MODULES_FILENAME)
let modules!: Modules
try {
const modules = await readYamlFile<Modules>(modulesYamlPath)
if (!modules.virtualStoreDir) {
modules.virtualStoreDir = path.join(modulesDir, '.pnpm')
} else if (!path.isAbsolute(modules.virtualStoreDir)) {
modules.virtualStoreDir = path.join(modulesDir, modules.virtualStoreDir)
}
return modules
modules = await readYamlFile<Modules>(modulesYamlPath)
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
throw err
}
return null
}
if (!modules.virtualStoreDir) {
modules.virtualStoreDir = path.join(modulesDir, '.pnpm')
} else if (!path.isAbsolute(modules.virtualStoreDir)) {
modules.virtualStoreDir = path.join(modulesDir, modules.virtualStoreDir)
}
switch (modules.shamefullyHoist) {
case true:
if (!modules.publicHoistPattern) {
modules.publicHoistPattern = ['*']
}
if (modules.hoistedAliases && !modules.hoistedDependencies) {
modules.hoistedDependencies = {}
for (const depPath of Object.keys(modules.hoistedAliases)) {
modules.hoistedDependencies[depPath] = {}
for (const alias of modules.hoistedAliases[depPath]) {
modules.hoistedDependencies[depPath][alias] = 'public'
}
}
}
break
case false:
if (!modules.publicHoistPattern) {
modules.publicHoistPattern = []
}
if (modules.hoistedAliases && !modules.hoistedDependencies) {
modules.hoistedDependencies = {}
for (const depPath of Object.keys(modules.hoistedAliases)) {
modules.hoistedDependencies[depPath] = {}
for (const alias of modules.hoistedAliases[depPath]) {
modules.hoistedDependencies[depPath][alias] = 'private'
}
}
}
break
}
return modules
}
const YAML_OPTS = { sortKeys: true }
const YAML_OPTS = {
noCompatMode: true,
noRefs: true,
sortKeys: true,
}
export function write (
modulesDir: string,
@@ -57,6 +94,11 @@ export function write (
if (!saveModules.hoistPattern) {
// Because the YAML writer fails on undefined fields
delete saveModules.hoistPattern
}
if (!saveModules.publicHoistPattern) {
delete saveModules.publicHoistPattern
}
if (!saveModules.hoistedAliases || !saveModules.hoistPattern && !saveModules.publicHoistPattern) {
delete saveModules.hoistedAliases
}
// We should store the absolute virtual store directory path on Windows

View File

@@ -0,0 +1,23 @@
hoistPattern:
- '*'
hoistedAliases:
/accepts/1.3.7:
- accepts
/array-flatten/1.1.1:
- array-flatten
/body-parser/1.19.0:
- body-parser
included:
dependencies: true
devDependencies: true
optionalDependencies: true
layoutVersion: 4
packageManager: pnpm@5.1.8
pendingBuilds: []
registries:
default: 'https://registry.npmjs.org/'
shamefullyHoist: false
skipped: []
storeDir: /home/zoli/.pnpm-store/v3
virtualStoreDir: .pnpm

View File

@@ -0,0 +1,23 @@
hoistPattern:
- '*'
hoistedAliases:
/accepts/1.3.7:
- accepts
/array-flatten/1.1.1:
- array-flatten
/body-parser/1.19.0:
- body-parser
included:
dependencies: true
devDependencies: true
optionalDependencies: true
layoutVersion: 4
packageManager: pnpm@5.1.8
pendingBuilds: []
registries:
default: 'https://registry.npmjs.org/'
shamefullyHoist: true
skipped: []
storeDir: /home/zoli/.pnpm-store/v3
virtualStoreDir: .pnpm

View File

@@ -9,7 +9,7 @@ import tempy = require('tempy')
test('write() and read()', async (t) => {
const modulesDir = tempy.directory()
const modulesYaml = {
hoistedAliases: {},
hoistedDependencies: {},
included: {
dependencies: true,
devDependencies: true,
@@ -18,6 +18,7 @@ test('write() and read()', async (t) => {
layoutVersion: 1,
packageManager: 'pnpm@2',
pendingBuilds: [],
publicHoistPattern: [],
registries: {
default: 'https://registry.npmjs.org/',
},
@@ -27,7 +28,6 @@ test('write() and read()', async (t) => {
virtualStoreDir: path.join(modulesDir, '.pnpm'),
}
await write(modulesDir, modulesYaml)
delete modulesYaml.hoistedAliases
t.deepEqual(await read(modulesDir), modulesYaml)
const raw = await readYamlFile(path.join(modulesDir, '.modules.yaml'))
@@ -36,3 +36,25 @@ test('write() and read()', async (t) => {
t.end()
})
test('backward compatible read of .modules.yaml created with shamefully-hoist=true', async (t) => {
const modulesYaml = await read(path.join(__dirname, 'fixtures/old-shamefully-hoist'))
t.deepEqual(modulesYaml.publicHoistPattern, ['*'])
t.deepEqual(modulesYaml.hoistedDependencies, {
'/accepts/1.3.7': { accepts: 'public' },
'/array-flatten/1.1.1': { 'array-flatten': 'public' },
'/body-parser/1.19.0': { 'body-parser': 'public' },
})
t.end()
})
test('backward compatible read of .modules.yaml created with shamefully-hoist=false', async (t) => {
const modulesYaml = await read(path.join(__dirname, 'fixtures/old-no-shamefully-hoist'))
t.deepEqual(modulesYaml.publicHoistPattern, [])
t.deepEqual(modulesYaml.hoistedDependencies, {
'/accepts/1.3.7': { accepts: 'private' },
'/array-flatten/1.1.1': { 'array-flatten': 'private' },
'/body-parser/1.19.0': { 'body-parser': 'private' },
})
t.end()
})

View File

@@ -33,6 +33,7 @@ export function rcOptionsTypes () {
'pnpmfile',
'prefer-offline',
'production',
'public-hoist-pattern',
'registry',
'reporter',
'save-dev',

View File

@@ -34,6 +34,7 @@ export function rcOptionsTypes () {
'prefer-frozen-lockfile',
'prefer-offline',
'production',
'public-hoist-pattern',
'registry',
'reporter',
'shamefully-flatten',
@@ -132,7 +133,7 @@ export function help () {
name: '--no-hoist',
},
{
description: 'The subdeps will be hoisted into the root node_modules. Your code will have access to them',
description: 'All the subdeps will be hoisted into the root node_modules. Your code will have access to them',
name: '--shamefully-hoist',
},
{
@@ -142,6 +143,10 @@ export function help () {
by any dependencies, so it is an emulation of a flat node_modules`,
name: '--hoist-pattern <pattern>',
},
{
description: `Hoist all dependencies matching the pattern to the root of the modules directory`,
name: '--public-hoist-pattern <pattern>',
},
OPTIONS.storeDir,
OPTIONS.virtualStoreDir,
{

View File

@@ -144,8 +144,10 @@ export default async function handler (
storeDir: store.dir,
workspacePackages,
forceHoistPattern: typeof opts.rawLocalConfig['hoist-pattern'] !== 'undefined' || typeof opts.rawLocalConfig['hoist'] !== 'undefined',
forceShamefullyHoist: typeof opts.rawLocalConfig['shamefully-hoist'] !== 'undefined',
forceHoistPattern: typeof opts.rawLocalConfig['hoist-pattern'] !== 'undefined'
|| typeof opts.rawLocalConfig['hoist'] !== 'undefined',
forcePublicHoistPattern: typeof opts.rawLocalConfig['shamefully-hoist'] !== 'undefined'
|| typeof opts.rawLocalConfig['public-hoist-pattern'] !== 'undefined',
}
if (!opts.ignorePnpmfile) {
installOpts['hooks'] = requireHooks(opts.lockfileDir || dir, opts)

View File

@@ -172,14 +172,14 @@ export async function rebuild (
await writeModulesYaml(ctx.rootModulesDir, {
...ctx.modulesFile,
hoistedAliases: ctx.hoistedAliases,
hoistedDependencies: ctx.hoistedDependencies,
hoistPattern: ctx.hoistPattern,
included: ctx.include,
layoutVersion: LAYOUT_VERSION,
packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`,
pendingBuilds: ctx.pendingBuilds,
publicHoistPattern: ctx.publicHoistPattern,
registries: ctx.registries,
shamefullyHoist: ctx.shamefullyHoist,
skipped: Array.from(ctx.skipped),
storeDir: ctx.storeDir,
virtualStoreDir: ctx.virtualStoreDir,

View File

@@ -112,10 +112,10 @@ test('command does not fail when deprecated options are used', async (t) => {
t.ok(stdout.toString().includes("Deprecated options: 'lock', 'independent-leaves'"))
})
test('adding new dep does not fail if node_modules was created with --hoist-pattern=eslint-* and --shamefully-hoist', async (t: tape.Test) => {
test('adding new dep does not fail if node_modules was created with --public-hoist-pattern=eslint-*', async (t: tape.Test) => {
const project = prepare(t)
await execPnpm(['add', 'is-positive', '--hoist-pattern=eslint-*', '--shamefully-hoist'])
await execPnpm(['add', 'is-positive', '--public-hoist-pattern=eslint-*'])
t.equal(execPnpmSync(['add', 'is-negative', '--no-hoist']).status, 1)
t.equal(execPnpmSync(['add', 'is-negative', '--no-shamefully-hoist']).status, 1)

View File

@@ -1,7 +1,7 @@
import { getLockfileImporterId } from '@pnpm/lockfile-file'
import { Modules, read as readModulesYaml } from '@pnpm/modules-yaml'
import normalizeRegistries from '@pnpm/normalize-registries'
import { DependenciesField, Registries } from '@pnpm/types'
import { DependenciesField, HoistedDependencies, Registries } from '@pnpm/types'
import path = require('path')
import realpathMissing = require('realpath-missing')
@@ -19,8 +19,9 @@ export default async function <T>(
}
): Promise<{
currentHoistPattern?: string[],
currentPublicHoistPattern?: string[],
hoist?: boolean,
hoistedAliases: { [depPath: string]: string[] },
hoistedDependencies: HoistedDependencies,
projects: Array<{
id: string,
} & T & Required<ProjectOptions>>,
@@ -29,16 +30,16 @@ export default async function <T>(
pendingBuilds: string[],
registries: Registries | null | undefined,
rootModulesDir: string,
shamefullyHoist?: boolean,
skipped: Set<string>,
}> {
const relativeModulesDir = opts.modulesDir ?? 'node_modules'
const rootModulesDir = await realpathMissing(path.join(opts.lockfileDir, relativeModulesDir))
const modules = await readModulesYaml(rootModulesDir)
return {
currentHoistPattern: modules?.hoistPattern || undefined,
currentHoistPattern: modules?.hoistPattern,
currentPublicHoistPattern: modules?.publicHoistPattern,
hoist: !modules ? undefined : Boolean(modules.hoistPattern),
hoistedAliases: modules?.hoistedAliases || {},
hoistedDependencies: modules?.hoistedDependencies ?? {},
include: modules?.included || { dependencies: true, devDependencies: true, optionalDependencies: true },
modules,
pendingBuilds: modules?.pendingBuilds || [],
@@ -56,7 +57,6 @@ export default async function <T>(
})),
registries: modules?.registries && normalizeRegistries(modules.registries),
rootModulesDir,
shamefullyHoist: modules?.shamefullyHoist || undefined,
skipped: new Set(modules?.skipped || []),
}
}

View File

@@ -30,7 +30,6 @@
"@pnpm/lockfile-utils": "workspace:2.0.13",
"@pnpm/lockfile-walker": "workspace:3.0.1",
"@pnpm/manifest-utils": "workspace:1.0.1",
"@pnpm/matcher": "workspace:1.0.2",
"@pnpm/modules-cleaner": "workspace:9.0.2",
"@pnpm/modules-yaml": "workspace:7.0.0",
"@pnpm/normalize-registries": "workspace:1.0.1",

View File

@@ -193,7 +193,7 @@ export async function mutateModules (
engineStrict: opts.engineStrict,
extraBinPaths: opts.extraBinPaths,
force: opts.force,
hoistedAliases: ctx.hoistedAliases,
hoistedDependencies: ctx.hoistedDependencies,
hoistPattern: ctx.hoistPattern,
ignoreScripts: opts.ignoreScripts,
include: opts.include,
@@ -212,9 +212,9 @@ export async function mutateModules (
pruneDirectDependencies?: boolean,
}>,
pruneStore: opts.pruneStore,
publicHoistPattern: ctx.publicHoistPattern,
rawConfig: opts.rawConfig,
registries: opts.registries,
shamefullyHoist: ctx.shamefullyHoist,
sideEffectsCacheRead: opts.sideEffectsCacheRead,
sideEffectsCacheWrite: opts.sideEffectsCacheWrite,
skipped: ctx.skipped,
@@ -693,7 +693,7 @@ async function installInContext (
currentLockfile: ctx.currentLockfile,
dryRun: opts.lockfileOnly,
force: opts.force,
hoistedAliases: ctx.hoistedAliases,
hoistedDependencies: ctx.hoistedDependencies,
hoistedModulesDir: ctx.hoistedModulesDir,
hoistPattern: ctx.hoistPattern,
include: opts.include,
@@ -701,7 +701,9 @@ async function installInContext (
makePartialCurrentLockfile: opts.makePartialCurrentLockfile,
outdatedDependencies,
pruneStore: opts.pruneStore,
publicHoistPattern: ctx.publicHoistPattern,
registries: ctx.registries,
rootModulesDir: ctx.rootModulesDir,
sideEffectsCacheRead: opts.sideEffectsCacheRead,
skipped: ctx.skipped,
storeController: opts.storeController,
@@ -799,14 +801,14 @@ async function installInContext (
}
return writeModulesYaml(ctx.rootModulesDir, {
...ctx.modulesFile,
hoistedAliases: result.newHoistedAliases,
hoistedDependencies: result.newHoistedDependencies,
hoistPattern: ctx.hoistPattern,
included: ctx.include,
layoutVersion: LAYOUT_VERSION,
packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`,
pendingBuilds: ctx.pendingBuilds,
publicHoistPattern: ctx.publicHoistPattern,
registries: ctx.registries,
shamefullyHoist: ctx.shamefullyHoist,
skipped: Array.from(ctx.skipped),
storeDir: ctx.storeDir,
virtualStoreDir: ctx.virtualStoreDir,

View File

@@ -14,13 +14,16 @@ import {
import hoist from '@pnpm/hoist'
import { Lockfile } from '@pnpm/lockfile-file'
import logger from '@pnpm/logger'
import matcher from '@pnpm/matcher'
import { prune } from '@pnpm/modules-cleaner'
import { IncludedDependencies } from '@pnpm/modules-yaml'
import { DependenciesTree, LinkedDependency } from '@pnpm/resolve-dependencies'
import { StoreController } from '@pnpm/store-controller-types'
import symlinkDependency, { symlinkDirectRootDependency } from '@pnpm/symlink-dependency'
import { ProjectManifest, Registries } from '@pnpm/types'
import {
HoistedDependencies,
ProjectManifest,
Registries,
} from '@pnpm/types'
import fs = require('mz/fs')
import pLimit = require('p-limit')
import path = require('path')
@@ -60,15 +63,17 @@ export default async function linkPackages (
currentLockfile: Lockfile,
dryRun: boolean,
force: boolean,
hoistedAliases: {[depPath: string]: string[]},
hoistedDependencies: HoistedDependencies,
hoistedModulesDir: string,
hoistPattern?: string[],
publicHoistPattern?: string[],
include: IncludedDependencies,
lockfileDir: string,
makePartialCurrentLockfile: boolean,
outdatedDependencies: {[pkgId: string]: string},
pruneStore: boolean,
registries: Registries,
rootModulesDir: string,
sideEffectsCacheRead: boolean,
skipped: Set<string>,
storeController: StoreController,
@@ -83,7 +88,7 @@ export default async function linkPackages (
currentLockfile: Lockfile,
depGraph: DependenciesGraph,
newDepPaths: string[],
newHoistedAliases: {[depPath: string]: string[]},
newHoistedDependencies: HoistedDependencies,
removedDepPaths: Set<string>,
wantedLockfile: Lockfile,
}> {
@@ -148,11 +153,12 @@ export default async function linkPackages (
const removedDepPaths = await prune(projects, {
currentLockfile: opts.currentLockfile,
dryRun: opts.dryRun,
hoistedAliases: opts.hoistedAliases,
hoistedDependencies: opts.hoistedDependencies,
hoistedModulesDir: opts.hoistPattern && opts.hoistedModulesDir || undefined,
include: opts.include,
lockfileDir: opts.lockfileDir,
pruneStore: opts.pruneStore,
publicHoistedModulesDir: opts.publicHoistPattern && opts.rootModulesDir || undefined,
registries: opts.registries,
skipped: opts.skipped,
storeController: opts.storeController,
@@ -303,15 +309,19 @@ export default async function linkPackages (
currentLockfile = newCurrentLockfile
}
let newHoistedAliases: Record<string, string[]> = {}
if (opts.hoistPattern && (newDepPaths.length > 0 || removedDepPaths.size > 0)) {
newHoistedAliases = await hoist(matcher(opts.hoistPattern!), {
let newHoistedDependencies!: HoistedDependencies
if ((opts.hoistPattern || opts.publicHoistPattern) && (newDepPaths.length > 0 || removedDepPaths.size > 0)) {
newHoistedDependencies = await hoist({
lockfile: currentLockfile,
lockfileDir: opts.lockfileDir,
modulesDir: opts.hoistedModulesDir,
registries: opts.registries,
privateHoistedModulesDir: opts.hoistedModulesDir,
privateHoistPattern: opts.hoistPattern ?? [],
publicHoistedModulesDir: opts.rootModulesDir,
publicHoistPattern: opts.publicHoistPattern ?? [],
virtualStoreDir: opts.virtualStoreDir,
})
} else {
newHoistedDependencies = {}
}
if (!opts.dryRun) {
@@ -333,7 +343,7 @@ export default async function linkPackages (
currentLockfile,
depGraph,
newDepPaths,
newHoistedAliases,
newHoistedDependencies,
removedDepPaths,
wantedLockfile: newWantedLockfile,
}

View File

@@ -110,10 +110,11 @@ export default async function link (
],
{
currentLockfile,
hoistedAliases: ctx.hoistedAliases,
hoistedDependencies: ctx.hoistedDependencies,
hoistedModulesDir: opts.hoistPattern && ctx.hoistedModulesDir || undefined,
include: ctx.include,
lockfileDir: opts.lockfileDir,
publicHoistedModulesDir: opts.publicHoistPattern && ctx.rootModulesDir || undefined,
registries: ctx.registries,
skipped: ctx.skipped,
storeController: opts.storeController,

View File

@@ -26,8 +26,8 @@ interface StrictLinkOptions {
hoistPattern: string[] | undefined,
forceHoistPattern: boolean,
shamefullyHoist: boolean,
forceShamefullyHoist: boolean,
publicHoistPattern: string[] | undefined,
forcePublicHoistPattern: boolean,
}
export type LinkOptions = Partial<StrictLinkOptions> &
@@ -57,7 +57,6 @@ async function defaults (opts: LinkOptions) {
hoistPattern: undefined,
lockfileDir: opts.lockfileDir || dir,
registries: DEFAULT_REGISTRIES,
shamefullyHoist: false,
storeController: opts.storeController,
storeDir: opts.storeDir,
useLockfile: true,

View File

@@ -32,12 +32,12 @@ test('should hoist dependencies', async (t) => {
await project.isExecutable('.pnpm/node_modules/.bin/mime')
})
test('should shamefully hoist dependencies', async (t) => {
test('should hoist dependencies to the root of node_modules when publicHoistPattern is used', async (t) => {
const project = prepareEmpty(t)
await addDependenciesToPackage({},
['express', '@foo/has-dep-from-same-scope'],
await testDefaults({ fastUnpack: false, hoistPattern: '*', shamefullyHoist: true }))
await testDefaults({ fastUnpack: false, publicHoistPattern: '*' }))
await project.has('express')
await project.has('debug')
@@ -50,6 +50,24 @@ test('should shamefully hoist dependencies', async (t) => {
await project.isExecutable('.bin/mime')
})
test('should hoist some dependencies to the root of node_modules when publicHoistPattern is used and others to the virtual store directory', async (t) => {
const project = prepareEmpty(t)
await addDependenciesToPackage({},
['express', '@foo/has-dep-from-same-scope'],
await testDefaults({ fastUnpack: false, hoistPattern: '*', publicHoistPattern: '@foo/*' }))
await project.has('express')
await project.has('.pnpm/node_modules/debug')
await project.has('.pnpm/node_modules/cookie')
await project.has('.pnpm/node_modules/mime')
await project.has('@foo/has-dep-from-same-scope')
await project.has('@foo/no-deps')
// should also hoist bins
await project.isExecutable('.pnpm/node_modules/.bin/mime')
})
test('should hoist dependencies by pattern', async (t) => {
const project = prepareEmpty(t)
@@ -113,7 +131,7 @@ test('should rehoist when uninstalling a package', async (t: tape.Test) => {
const modules = await project.readModulesManifest()
t.ok(modules)
t.deepEqual(modules!.hoistedAliases[`/debug/2.6.9`], ['debug'], 'new hoisted debug added to .modules.yaml')
t.deepEqual(modules!.hoistedDependencies[`/debug/2.6.9`], { debug: 'private' }, 'new hoisted debug added to .modules.yaml')
})
test('should rehoist after running a general install', async (t) => {
@@ -167,8 +185,7 @@ test('hoistPattern=* throws exception when executed on node_modules installed w/
}))
t.fail('installation should have failed')
} catch (err) {
t.equal(err['code'], 'ERR_PNPM_HOISTING_NOT_WANTED') // tslint:disable-line:no-string-literal
t.ok(err.message.indexOf('This modules directory was created without the --hoist-pattern option.') === 0)
t.equal(err['code'], 'ERR_PNPM_HOIST_PATTERN_DIFF') // tslint:disable-line:no-string-literal
}
})
@@ -185,8 +202,7 @@ test('hoistPattern=undefined throws exception when executed on node_modules inst
})
t.fail('installation should have failed')
} catch (err) {
t.equal(err['code'], 'ERR_PNPM_HOISTING_WANTED') // tslint:disable-line:no-string-literal
t.ok(err.message.indexOf('This modules directory was created using the --hoist-pattern option.') === 0)
t.equal(err['code'], 'ERR_PNPM_HOIST_PATTERN_DIFF') // tslint:disable-line:no-string-literal
}
// Instatll doesn't fail if the value of hoistPattern isn't forced
@@ -210,7 +226,7 @@ test('hoist by alias', async (t: tape.Test) => {
const modules = await project.readModulesManifest()
t.ok(modules)
t.deepEqual(modules!.hoistedAliases, { [`/dep-of-pkg-with-1-dep/100.1.0`]: [ 'dep' ] }, '.modules.yaml updated correctly')
t.deepEqual(modules!.hoistedDependencies, { [`/dep-of-pkg-with-1-dep/100.1.0`]: { dep: 'private' } }, '.modules.yaml updated correctly')
})
test('should remove aliased hoisted dependencies', async (t) => {
@@ -238,7 +254,7 @@ test('should remove aliased hoisted dependencies', async (t) => {
const modules = await project.readModulesManifest()
t.ok(modules)
t.deepEqual(modules!.hoistedAliases, {}, '.modules.yaml updated correctly')
t.deepEqual(modules!.hoistedDependencies, {}, '.modules.yaml updated correctly')
})
test('should update .modules.yaml when pruning if we are flattening', async (t) => {
@@ -254,7 +270,7 @@ test('should update .modules.yaml when pruning if we are flattening', async (t)
const modules = await project.readModulesManifest()
t.ok(modules)
t.deepEqual(modules!.hoistedAliases, {}, '.modules.yaml updated correctly')
t.deepEqual(modules!.hoistedDependencies, {}, '.modules.yaml updated correctly')
})
test('should rehoist after pruning', async (t) => {
@@ -432,9 +448,9 @@ test('hoist when updating in one of the workspace projects', async (t) => {
const rootModules = assertProject(t, process.cwd())
{
const modulesManifest = await rootModules.readModulesManifest()
t.deepEqual(modulesManifest?.hoistedAliases, {
[`/dep-of-pkg-with-1-dep/100.0.0`]: ['dep-of-pkg-with-1-dep'],
[`/foo/100.0.0`]: ['foo'],
t.deepEqual(modulesManifest?.hoistedDependencies, {
[`/dep-of-pkg-with-1-dep/100.0.0`]: { 'dep-of-pkg-with-1-dep': 'private' },
[`/foo/100.0.0`]: { 'foo': 'private' },
})
}
@@ -460,8 +476,8 @@ test('hoist when updating in one of the workspace projects', async (t) => {
{
const modulesManifest = await rootModules.readModulesManifest()
t.deepEqual(modulesManifest?.hoistedAliases, {
[`/dep-of-pkg-with-1-dep/100.0.0`]: ['dep-of-pkg-with-1-dep'],
t.deepEqual(modulesManifest?.hoistedDependencies, {
[`/dep-of-pkg-with-1-dep/100.0.0`]: { 'dep-of-pkg-with-1-dep': 'private' },
})
}
})
@@ -474,7 +490,7 @@ test('should recreate node_modules with hoisting', async (t: tape.Test) => {
{
const modulesManifest = await project.readModulesManifest()
t.notOk(modulesManifest.hoistPattern)
t.notOk(modulesManifest.hoistedAliases)
t.deepEqual(modulesManifest.hoistedDependencies, {})
}
await mutateModules([
@@ -491,6 +507,6 @@ test('should recreate node_modules with hoisting', async (t: tape.Test) => {
{
const modulesManifest = await project.readModulesManifest()
t.ok(modulesManifest.hoistPattern)
t.ok(modulesManifest.hoistedAliases)
t.ok(Object.keys(modulesManifest.hoistedDependencies).length > 0)
}
})

View File

@@ -51,9 +51,6 @@
{
"path": "../manifest-utils"
},
{
"path": "../matcher"
},
{
"path": "../modules-cleaner"
},

View File

@@ -11,3 +11,5 @@ export interface Registries {
default: string,
[scope: string]: string,
}
export type HoistedDependencies = Record<string, Record<string, 'public' | 'private'>>

6
pnpm-lock.yaml generated
View File

@@ -612,7 +612,6 @@ importers:
'@pnpm/link-bins': 'link:../link-bins'
'@pnpm/lockfile-file': 'link:../lockfile-file'
'@pnpm/lockfile-utils': 'link:../lockfile-utils'
'@pnpm/matcher': 'link:../matcher'
'@pnpm/modules-cleaner': 'link:../modules-cleaner'
'@pnpm/modules-yaml': 'link:../modules-yaml'
'@pnpm/package-requester': 'link:../package-requester'
@@ -670,7 +669,6 @@ importers:
'@pnpm/lockfile-file': 'workspace:3.0.9'
'@pnpm/lockfile-utils': 'workspace:2.0.13'
'@pnpm/logger': 3.2.2
'@pnpm/matcher': 'workspace:1.0.2'
'@pnpm/modules-cleaner': 'workspace:9.0.2'
'@pnpm/modules-yaml': 'workspace:7.0.0'
'@pnpm/package-requester': 'workspace:12.0.4'
@@ -713,6 +711,7 @@ importers:
'@pnpm/lockfile-types': 'link:../lockfile-types'
'@pnpm/lockfile-utils': 'link:../lockfile-utils'
'@pnpm/lockfile-walker': 'link:../lockfile-walker'
'@pnpm/matcher': 'link:../matcher'
'@pnpm/pkgid-to-filename': 3.0.0
'@pnpm/symlink-dependency': 'link:../symlink-dependency'
'@pnpm/types': 'link:../types'
@@ -728,6 +727,7 @@ importers:
'@pnpm/lockfile-utils': 'workspace:2.0.13'
'@pnpm/lockfile-walker': 'workspace:3.0.1'
'@pnpm/logger': 3.2.2
'@pnpm/matcher': 'workspace:^1.0.2'
'@pnpm/pkgid-to-filename': 3.0.0
'@pnpm/symlink-dependency': 'workspace:3.0.6'
'@pnpm/types': 'workspace:6.0.0'
@@ -2576,7 +2576,6 @@ importers:
'@pnpm/lockfile-utils': 'link:../lockfile-utils'
'@pnpm/lockfile-walker': 'link:../lockfile-walker'
'@pnpm/manifest-utils': 'link:../manifest-utils'
'@pnpm/matcher': 'link:../matcher'
'@pnpm/modules-cleaner': 'link:../modules-cleaner'
'@pnpm/modules-yaml': 'link:../modules-yaml'
'@pnpm/normalize-registries': 'link:../normalize-registries'
@@ -2678,7 +2677,6 @@ importers:
'@pnpm/lockfile-walker': 'workspace:3.0.1'
'@pnpm/logger': 3.2.2
'@pnpm/manifest-utils': 'workspace:1.0.1'
'@pnpm/matcher': 'workspace:1.0.2'
'@pnpm/modules-cleaner': 'workspace:9.0.2'
'@pnpm/modules-yaml': 'workspace:7.0.0'
'@pnpm/normalize-registries': 'workspace:1.0.1'