mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-24 07:38:12 -05:00
7
.changeset/light-eagles-tan.md
Normal file
7
.changeset/light-eagles-tan.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@pnpm/config": minor
|
||||
"@pnpm/headless": minor
|
||||
"supi": patch
|
||||
---
|
||||
|
||||
New option added: enableModulesDir. When `false`, pnpm will not write any files to the modules directory. This is useful for when you want to mount the modules directory with FUSE.
|
||||
@@ -117,6 +117,7 @@ export interface Config {
|
||||
recursiveInstall?: boolean
|
||||
symlink: boolean
|
||||
enablePnp?: boolean
|
||||
enableModulesDir: boolean
|
||||
|
||||
registries: Registries
|
||||
ignoreWorkspaceRootCheck: boolean
|
||||
|
||||
@@ -32,6 +32,7 @@ export const types = Object.assign({
|
||||
color: ['always', 'auto', 'never'],
|
||||
dev: [null, true],
|
||||
dir: String,
|
||||
'enable-modules-dir': Boolean,
|
||||
'fetching-concurrency': Number,
|
||||
filter: [String, Array],
|
||||
'frozen-lockfile': Boolean,
|
||||
@@ -146,6 +147,7 @@ export default async (
|
||||
const npmConfig = loadNpmConf(cliOptions, rcOptionsTypes, {
|
||||
bail: true,
|
||||
color: 'auto',
|
||||
'enable-modules-dir': true,
|
||||
'fetch-retries': 2,
|
||||
'fetch-retry-factor': 10,
|
||||
'fetch-retry-maxtimeout': 60000,
|
||||
|
||||
@@ -116,6 +116,7 @@ export interface HeadlessOptions {
|
||||
ownLifecycleHooksStdio?: 'inherit' | 'pipe'
|
||||
pendingBuilds: string[]
|
||||
skipped: Set<string>
|
||||
enableModulesDir?: boolean
|
||||
}
|
||||
|
||||
export default async (opts: HeadlessOptions) => {
|
||||
@@ -245,26 +246,6 @@ export default async (opts: HeadlessOptions) => {
|
||||
prefix: lockfileDir,
|
||||
})
|
||||
|
||||
await Promise.all(depNodes.map((depNode) => fs.mkdir(depNode.modules, { recursive: true })))
|
||||
await Promise.all([
|
||||
opts.symlink === false
|
||||
? Promise.resolve()
|
||||
: linkAllModules(depNodes, {
|
||||
lockfileDir,
|
||||
optional: opts.include.optionalDependencies,
|
||||
}),
|
||||
linkAllPkgs(opts.storeController, depNodes, {
|
||||
force: opts.force,
|
||||
lockfileDir: opts.lockfileDir,
|
||||
targetEngine: opts.sideEffectsCacheRead && ENGINE_NAME || undefined,
|
||||
}),
|
||||
])
|
||||
|
||||
stageLogger.debug({
|
||||
prefix: lockfileDir,
|
||||
stage: 'importing_done',
|
||||
})
|
||||
|
||||
function warn (message: string) {
|
||||
logger.info({
|
||||
message,
|
||||
@@ -272,134 +253,156 @@ export default async (opts: HeadlessOptions) => {
|
||||
})
|
||||
}
|
||||
|
||||
let newHoistedDependencies!: HoistedDependencies
|
||||
if (opts.hoistPattern != null || opts.publicHoistPattern != null) {
|
||||
newHoistedDependencies = await hoist({
|
||||
lockfile: filteredLockfile,
|
||||
lockfileDir,
|
||||
privateHoistedModulesDir: hoistedModulesDir,
|
||||
privateHoistPattern: opts.hoistPattern ?? [],
|
||||
publicHoistedModulesDir,
|
||||
publicHoistPattern: opts.publicHoistPattern ?? [],
|
||||
virtualStoreDir,
|
||||
})
|
||||
} else {
|
||||
newHoistedDependencies = {}
|
||||
}
|
||||
if (opts.enableModulesDir !== false) {
|
||||
await Promise.all(depNodes.map((depNode) => fs.mkdir(depNode.modules, { recursive: true })))
|
||||
await Promise.all([
|
||||
opts.symlink === false
|
||||
? Promise.resolve()
|
||||
: linkAllModules(depNodes, {
|
||||
lockfileDir,
|
||||
optional: opts.include.optionalDependencies,
|
||||
}),
|
||||
linkAllPkgs(opts.storeController, depNodes, {
|
||||
force: opts.force,
|
||||
lockfileDir: opts.lockfileDir,
|
||||
targetEngine: opts.sideEffectsCacheRead && ENGINE_NAME || undefined,
|
||||
}),
|
||||
])
|
||||
|
||||
await Promise.all(opts.projects.map(async ({ rootDir, id, manifest, modulesDir }) => {
|
||||
if (opts.symlink !== false) {
|
||||
await linkRootPackages(filteredLockfile, {
|
||||
importerId: id,
|
||||
importerModulesDir: modulesDir,
|
||||
stageLogger.debug({
|
||||
prefix: lockfileDir,
|
||||
stage: 'importing_done',
|
||||
})
|
||||
|
||||
let newHoistedDependencies!: HoistedDependencies
|
||||
if (opts.hoistPattern != null || opts.publicHoistPattern != null) {
|
||||
newHoistedDependencies = await hoist({
|
||||
lockfile: filteredLockfile,
|
||||
lockfileDir,
|
||||
projectDir: rootDir,
|
||||
projects: opts.projects,
|
||||
registries: opts.registries,
|
||||
rootDependencies: directDependenciesByImporterId[id],
|
||||
privateHoistedModulesDir: hoistedModulesDir,
|
||||
privateHoistPattern: opts.hoistPattern ?? [],
|
||||
publicHoistedModulesDir,
|
||||
publicHoistPattern: opts.publicHoistPattern ?? [],
|
||||
virtualStoreDir,
|
||||
})
|
||||
} else {
|
||||
newHoistedDependencies = {}
|
||||
}
|
||||
|
||||
await Promise.all(opts.projects.map(async ({ rootDir, id, manifest, modulesDir }) => {
|
||||
if (opts.symlink !== false) {
|
||||
await linkRootPackages(filteredLockfile, {
|
||||
importerId: id,
|
||||
importerModulesDir: modulesDir,
|
||||
lockfileDir,
|
||||
projectDir: rootDir,
|
||||
projects: opts.projects,
|
||||
registries: opts.registries,
|
||||
rootDependencies: directDependenciesByImporterId[id],
|
||||
})
|
||||
}
|
||||
|
||||
// Even though headless installation will never update the package.json
|
||||
// this needs to be logged because otherwise install summary won't be printed
|
||||
packageManifestLogger.debug({
|
||||
prefix: rootDir,
|
||||
updated: manifest,
|
||||
})
|
||||
}))
|
||||
|
||||
if (opts.ignoreScripts) {
|
||||
for (const { id, manifest } of opts.projects) {
|
||||
if (opts.ignoreScripts && manifest?.scripts &&
|
||||
(manifest.scripts.preinstall ?? manifest.scripts.prepublish ??
|
||||
manifest.scripts.install ??
|
||||
manifest.scripts.postinstall ??
|
||||
manifest.scripts.prepare)
|
||||
) {
|
||||
opts.pendingBuilds.push(id)
|
||||
}
|
||||
}
|
||||
// we can use concat here because we always only append new packages, which are guaranteed to not be there by definition
|
||||
opts.pendingBuilds = opts.pendingBuilds
|
||||
.concat(
|
||||
depNodes
|
||||
.filter(({ requiresBuild }) => requiresBuild)
|
||||
.map(({ depPath }) => depPath)
|
||||
)
|
||||
} else {
|
||||
const directNodes = new Set<string>()
|
||||
for (const { id } of opts.projects) {
|
||||
R
|
||||
.values(directDependenciesByImporterId[id])
|
||||
.filter((loc) => graph[loc])
|
||||
.forEach((loc) => {
|
||||
directNodes.add(loc)
|
||||
})
|
||||
}
|
||||
const extraBinPaths = [...opts.extraBinPaths ?? []]
|
||||
if (opts.hoistPattern) {
|
||||
extraBinPaths.unshift(path.join(virtualStoreDir, 'node_modules/.bin'))
|
||||
}
|
||||
let extraEnv: Record<string, string> | undefined
|
||||
if (opts.enablePnp) {
|
||||
extraEnv = makeNodeRequireOption(path.join(opts.lockfileDir, '.pnp.js'))
|
||||
}
|
||||
await buildModules(graph, Array.from(directNodes), {
|
||||
childConcurrency: opts.childConcurrency,
|
||||
extraBinPaths,
|
||||
extraEnv,
|
||||
lockfileDir,
|
||||
optional: opts.include.optionalDependencies,
|
||||
rawConfig: opts.rawConfig,
|
||||
rootModulesDir: virtualStoreDir,
|
||||
scriptShell: opts.scriptShell,
|
||||
shellEmulator: opts.shellEmulator,
|
||||
sideEffectsCacheWrite: opts.sideEffectsCacheWrite,
|
||||
storeController: opts.storeController,
|
||||
unsafePerm: opts.unsafePerm,
|
||||
userAgent: opts.userAgent,
|
||||
})
|
||||
}
|
||||
|
||||
// Even though headless installation will never update the package.json
|
||||
// this needs to be logged because otherwise install summary won't be printed
|
||||
packageManifestLogger.debug({
|
||||
prefix: rootDir,
|
||||
updated: manifest,
|
||||
})
|
||||
}))
|
||||
|
||||
if (opts.ignoreScripts) {
|
||||
for (const { id, manifest } of opts.projects) {
|
||||
if (opts.ignoreScripts && manifest?.scripts &&
|
||||
(manifest.scripts.preinstall ?? manifest.scripts.prepublish ??
|
||||
manifest.scripts.install ??
|
||||
manifest.scripts.postinstall ??
|
||||
manifest.scripts.prepare)
|
||||
) {
|
||||
opts.pendingBuilds.push(id)
|
||||
}
|
||||
}
|
||||
// we can use concat here because we always only append new packages, which are guaranteed to not be there by definition
|
||||
opts.pendingBuilds = opts.pendingBuilds
|
||||
.concat(
|
||||
depNodes
|
||||
.filter(({ requiresBuild }) => requiresBuild)
|
||||
.map(({ depPath }) => depPath)
|
||||
)
|
||||
} else {
|
||||
const directNodes = new Set<string>()
|
||||
for (const { id } of opts.projects) {
|
||||
R
|
||||
.values(directDependenciesByImporterId[id])
|
||||
.filter((loc) => graph[loc])
|
||||
.forEach((loc) => {
|
||||
directNodes.add(loc)
|
||||
})
|
||||
}
|
||||
const extraBinPaths = [...opts.extraBinPaths ?? []]
|
||||
if (opts.hoistPattern) {
|
||||
extraBinPaths.unshift(path.join(virtualStoreDir, 'node_modules/.bin'))
|
||||
}
|
||||
let extraEnv: Record<string, string> | undefined
|
||||
if (opts.enablePnp) {
|
||||
extraEnv = makeNodeRequireOption(path.join(opts.lockfileDir, '.pnp.js'))
|
||||
}
|
||||
await buildModules(graph, Array.from(directNodes), {
|
||||
childConcurrency: opts.childConcurrency,
|
||||
extraBinPaths,
|
||||
extraEnv,
|
||||
lockfileDir,
|
||||
optional: opts.include.optionalDependencies,
|
||||
rawConfig: opts.rawConfig,
|
||||
rootModulesDir: virtualStoreDir,
|
||||
scriptShell: opts.scriptShell,
|
||||
shellEmulator: opts.shellEmulator,
|
||||
sideEffectsCacheWrite: opts.sideEffectsCacheWrite,
|
||||
storeController: opts.storeController,
|
||||
unsafePerm: opts.unsafePerm,
|
||||
userAgent: opts.userAgent,
|
||||
})
|
||||
}
|
||||
|
||||
await linkAllBins(graph, { optional: opts.include.optionalDependencies, warn })
|
||||
await Promise.all(opts.projects.map(async (project) => {
|
||||
if (opts.publicHoistPattern?.length && path.relative(opts.lockfileDir, project.rootDir) === '') {
|
||||
await linkBinsOfImporter(project)
|
||||
} else {
|
||||
const directPkgDirs = Object.values(directDependenciesByImporterId[project.id])
|
||||
await linkBinsOfPackages(
|
||||
(
|
||||
await Promise.all(
|
||||
directPkgDirs.map(async (dir) => ({
|
||||
location: dir,
|
||||
manifest: await safeReadProjectManifestOnly(dir),
|
||||
}))
|
||||
await linkAllBins(graph, { optional: opts.include.optionalDependencies, warn })
|
||||
await Promise.all(opts.projects.map(async (project) => {
|
||||
if (opts.publicHoistPattern?.length && path.relative(opts.lockfileDir, project.rootDir) === '') {
|
||||
await linkBinsOfImporter(project)
|
||||
} else {
|
||||
const directPkgDirs = Object.values(directDependenciesByImporterId[project.id])
|
||||
await linkBinsOfPackages(
|
||||
(
|
||||
await Promise.all(
|
||||
directPkgDirs.map(async (dir) => ({
|
||||
location: dir,
|
||||
manifest: await safeReadProjectManifestOnly(dir),
|
||||
}))
|
||||
)
|
||||
)
|
||||
.filter(({ manifest }) => manifest != null) as Array<{ location: string, manifest: DependencyManifest }>,
|
||||
project.binsDir,
|
||||
{ warn: (message: string) => logger.info({ message, prefix: project.rootDir }) }
|
||||
)
|
||||
.filter(({ manifest }) => manifest != null) as Array<{ location: string, manifest: DependencyManifest }>,
|
||||
project.binsDir,
|
||||
{ warn: (message: string) => logger.info({ message, prefix: project.rootDir }) }
|
||||
)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
if (currentLockfile && !R.equals(opts.projects.map(({ id }) => id).sort(), Object.keys(filteredLockfile.importers).sort())) {
|
||||
Object.assign(filteredLockfile.packages, currentLockfile.packages)
|
||||
if (currentLockfile && !R.equals(opts.projects.map(({ id }) => id).sort(), Object.keys(filteredLockfile.importers).sort())) {
|
||||
Object.assign(filteredLockfile.packages, currentLockfile.packages)
|
||||
}
|
||||
await writeCurrentLockfile(virtualStoreDir, filteredLockfile)
|
||||
await writeModulesYaml(rootModulesDir, {
|
||||
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,
|
||||
skipped: Array.from(skipped),
|
||||
storeDir: opts.storeDir,
|
||||
virtualStoreDir,
|
||||
})
|
||||
}
|
||||
await writeCurrentLockfile(virtualStoreDir, filteredLockfile)
|
||||
await writeModulesYaml(rootModulesDir, {
|
||||
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,
|
||||
skipped: Array.from(skipped),
|
||||
storeDir: opts.storeDir,
|
||||
virtualStoreDir,
|
||||
})
|
||||
|
||||
// waiting till package requests are finished
|
||||
await Promise.all(depNodes.map(({ finishing }) => finishing))
|
||||
|
||||
@@ -721,3 +721,16 @@ test('installing with no symlinks but with PnP', async () => {
|
||||
expect(await project.readModulesManifest()).toBeTruthy()
|
||||
expect(await exists(path.join(prefix, '.pnp.js'))).toBeTruthy()
|
||||
})
|
||||
|
||||
test('installing with no modules directory', async () => {
|
||||
const prefix = path.join(fixtures, 'simple')
|
||||
await rimraf(path.join(prefix, 'node_modules'))
|
||||
await rimraf(path.join(prefix, '.pnp.js'))
|
||||
|
||||
await headless(await testDefaults({
|
||||
enableModulesDir: false,
|
||||
lockfileDir: prefix,
|
||||
}))
|
||||
|
||||
expect(await exists(path.join(prefix, 'node_modules'))).toBeFalsy()
|
||||
})
|
||||
|
||||
@@ -68,6 +68,7 @@ export interface StrictInstallOptions {
|
||||
virtualStoreDir?: string
|
||||
dir: string
|
||||
symlink: boolean
|
||||
enableModulesDir: boolean
|
||||
|
||||
hoistPattern: string[] | undefined
|
||||
forceHoistPattern: boolean
|
||||
@@ -142,6 +143,7 @@ const defaults = async (opts: InstallOptions) => {
|
||||
userAgent: `${packageManager.name}/${packageManager.version} npm/? node/${process.version} ${process.platform} ${process.arch}`,
|
||||
verifyStoreIntegrity: true,
|
||||
workspacePackages: {},
|
||||
enableModulesDir: true,
|
||||
} as StrictInstallOptions
|
||||
}
|
||||
|
||||
|
||||
@@ -699,7 +699,7 @@ async function installInContext (
|
||||
}
|
||||
|
||||
const lockfileOpts = { forceSharedFormat: opts.forceSharedLockfile }
|
||||
if (!opts.lockfileOnly) {
|
||||
if (!opts.lockfileOnly && opts.enableModulesDir) {
|
||||
const result = await linkPackages(
|
||||
projectsToResolve,
|
||||
dependenciesGraph,
|
||||
|
||||
@@ -1219,3 +1219,22 @@ test('installing with no symlinks with PnP', async () => {
|
||||
expect(await project.readModulesManifest()).toBeTruthy()
|
||||
expect(await exists(path.resolve('.pnp.js'))).toBeTruthy()
|
||||
})
|
||||
|
||||
test('installing with no modules directory', async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
await addDependenciesToPackage(
|
||||
{
|
||||
name: 'project',
|
||||
version: '0.0.0',
|
||||
},
|
||||
['rimraf@2.7.1'],
|
||||
await testDefaults({
|
||||
enableModulesDir: false,
|
||||
fastUnpack: false,
|
||||
})
|
||||
)
|
||||
|
||||
expect(await project.readLockfile()).toBeTruthy()
|
||||
expect(await exists(path.resolve('node_modules'))).toBeFalsy()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user