mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat: no symlink
This is a continuation of #1874. Setting `symlink=false` in an `.npmrc` file will allow experimenting with Plug'n'Play, which for now is only possible by manually generating the `.pnp.js` file using the `@pnpm/lockfile-to-pnp` package. This is probably a temporary setting. In the future, there will be something like `enable-pnp` instead. PR #2900
This commit is contained in:
7
.changeset/angry-cats-yell.md
Normal file
7
.changeset/angry-cats-yell.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@pnpm/config": minor
|
||||
"@pnpm/headless": minor
|
||||
"supi": patch
|
||||
---
|
||||
|
||||
New experimental option added for installing node_modules w/o symlinks.
|
||||
@@ -113,6 +113,7 @@ export interface Config {
|
||||
gitChecks?: boolean
|
||||
publishBranch?: string
|
||||
recursiveInstall?: boolean
|
||||
symlink: boolean
|
||||
|
||||
registries: Registries
|
||||
ignoreWorkspaceRootCheck: boolean
|
||||
|
||||
@@ -74,6 +74,7 @@ export const types = Object.assign({
|
||||
'shrinkwrap-only': Boolean,
|
||||
'side-effects-cache': Boolean,
|
||||
'side-effects-cache-readonly': Boolean,
|
||||
symlink: Boolean,
|
||||
sort: Boolean,
|
||||
store: String, // TODO: deprecate
|
||||
'store-dir': String,
|
||||
@@ -160,6 +161,7 @@ export default async (
|
||||
registry: npmDefaults.registry,
|
||||
'save-peer': false,
|
||||
'save-workspace-protocol': true,
|
||||
symlink: true,
|
||||
'shared-workspace-lockfile': true,
|
||||
'shared-workspace-shrinkwrap': true,
|
||||
'shell-emulator': false,
|
||||
@@ -348,6 +350,10 @@ export default async (
|
||||
}
|
||||
break
|
||||
}
|
||||
if (!pnpmConfig.symlink) {
|
||||
delete pnpmConfig.hoistPattern
|
||||
delete pnpmConfig.publicHoistPattern
|
||||
}
|
||||
if (typeof pnpmConfig['color'] === 'boolean') {
|
||||
switch (pnpmConfig['color']) {
|
||||
case true:
|
||||
|
||||
@@ -92,6 +92,7 @@ export interface HeadlessOptions {
|
||||
storeController: StoreController
|
||||
sideEffectsCacheRead: boolean
|
||||
sideEffectsCacheWrite: boolean
|
||||
symlink?: boolean
|
||||
force: boolean
|
||||
storeDir: string
|
||||
rawConfig: object
|
||||
@@ -219,10 +220,12 @@ export default async (opts: HeadlessOptions) => {
|
||||
|
||||
await Promise.all(depNodes.map((depNode) => fs.mkdir(depNode.modules, { recursive: true })))
|
||||
await Promise.all([
|
||||
linkAllModules(depNodes, {
|
||||
lockfileDir,
|
||||
optional: opts.include.optionalDependencies,
|
||||
}),
|
||||
opts.symlink === false
|
||||
? Promise.resolve()
|
||||
: linkAllModules(depNodes, {
|
||||
lockfileDir,
|
||||
optional: opts.include.optionalDependencies,
|
||||
}),
|
||||
linkAllPkgs(opts.storeController, depNodes, {
|
||||
force: opts.force,
|
||||
lockfileDir: opts.lockfileDir,
|
||||
@@ -258,15 +261,17 @@ export default async (opts: HeadlessOptions) => {
|
||||
}
|
||||
|
||||
await Promise.all(opts.projects.map(async ({ rootDir, id, manifest, modulesDir }) => {
|
||||
await linkRootPackages(filteredLockfile, {
|
||||
importerId: id,
|
||||
importerModulesDir: modulesDir,
|
||||
lockfileDir,
|
||||
projectDir: rootDir,
|
||||
projects: opts.projects,
|
||||
registries: opts.registries,
|
||||
rootDependencies: directDependenciesByImporterId[id],
|
||||
})
|
||||
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
|
||||
@@ -721,14 +726,14 @@ function linkAllBins (
|
||||
)
|
||||
}
|
||||
|
||||
function linkAllModules (
|
||||
async function linkAllModules (
|
||||
depNodes: DependenciesGraphNode[],
|
||||
opts: {
|
||||
optional: boolean
|
||||
lockfileDir: string
|
||||
}
|
||||
) {
|
||||
return Promise.all(
|
||||
await Promise.all(
|
||||
depNodes
|
||||
.map(async (depNode) => {
|
||||
const childrenToLink = opts.optional
|
||||
|
||||
@@ -751,3 +751,22 @@ test('installing in a workspace', async (t) => {
|
||||
|
||||
t.end()
|
||||
})
|
||||
|
||||
test('installing with no symlinks', async (t) => {
|
||||
const prefix = path.join(fixtures, 'simple')
|
||||
await rimraf(path.join(prefix, 'node_modules'))
|
||||
|
||||
await headless(await testDefaults({
|
||||
lockfileDir: prefix,
|
||||
symlink: false,
|
||||
}))
|
||||
|
||||
t.deepEqual(await fs.readdir(path.join(prefix, 'node_modules')), ['.modules.yaml', '.pnpm'])
|
||||
t.deepEqual(await fs.readdir(path.join(prefix, 'node_modules/.pnpm/rimraf@2.7.1/node_modules')), ['rimraf'])
|
||||
|
||||
const project = assertProject(t, prefix)
|
||||
t.ok(await project.readCurrentLockfile())
|
||||
t.ok(await project.readModulesManifest())
|
||||
|
||||
t.end()
|
||||
})
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface StrictInstallOptions {
|
||||
pruneStore: boolean
|
||||
virtualStoreDir?: string
|
||||
dir: string
|
||||
symlink: boolean
|
||||
|
||||
hoistPattern: string[] | undefined
|
||||
forceHoistPattern: boolean
|
||||
@@ -121,6 +122,7 @@ const defaults = async (opts: InstallOptions) => {
|
||||
shellEmulator: false,
|
||||
sideEffectsCacheRead: false,
|
||||
sideEffectsCacheWrite: false,
|
||||
symlink: true,
|
||||
storeController: opts.storeController,
|
||||
storeDir: opts.storeDir,
|
||||
strictPeerDependencies: false,
|
||||
|
||||
@@ -206,6 +206,7 @@ export async function mutateModules (
|
||||
registries: opts.registries,
|
||||
sideEffectsCacheRead: opts.sideEffectsCacheRead,
|
||||
sideEffectsCacheWrite: opts.sideEffectsCacheWrite,
|
||||
symlink: opts.symlink,
|
||||
skipped: ctx.skipped,
|
||||
storeController: opts.storeController,
|
||||
storeDir: opts.storeDir,
|
||||
@@ -666,6 +667,7 @@ async function installInContext (
|
||||
registries: ctx.registries,
|
||||
rootModulesDir: ctx.rootModulesDir,
|
||||
sideEffectsCacheRead: opts.sideEffectsCacheRead,
|
||||
symlink: opts.symlink,
|
||||
skipped: ctx.skipped,
|
||||
storeController: opts.storeController,
|
||||
strictPeerDependencies: opts.strictPeerDependencies,
|
||||
|
||||
@@ -54,6 +54,7 @@ export default async function linkPackages (
|
||||
registries: Registries
|
||||
rootModulesDir: string
|
||||
sideEffectsCacheRead: boolean
|
||||
symlink: boolean
|
||||
skipped: Set<string>
|
||||
storeController: StoreController
|
||||
strictPeerDependencies: boolean
|
||||
@@ -132,6 +133,7 @@ export default async function linkPackages (
|
||||
lockfileDir: opts.lockfileDir,
|
||||
optional: opts.include.optionalDependencies,
|
||||
sideEffectsCacheRead: opts.sideEffectsCacheRead,
|
||||
symlink: opts.symlink,
|
||||
skipped: opts.skipped,
|
||||
storeController: opts.storeController,
|
||||
virtualStoreDir: opts.virtualStoreDir,
|
||||
@@ -143,46 +145,48 @@ export default async function linkPackages (
|
||||
stage: 'importing_done',
|
||||
})
|
||||
|
||||
await Promise.all(projects.map(async ({ id, manifest, modulesDir, rootDir }) => {
|
||||
const deps = opts.dependenciesByProjectId[id]
|
||||
await Promise.all([
|
||||
...Object.entries(deps)
|
||||
.map(([rootAlias, depPath]) => ({ rootAlias, depGraphNode: depGraph[depPath] }))
|
||||
.filter(({ depGraphNode }) => depGraphNode)
|
||||
.map(async ({ rootAlias, depGraphNode }) => {
|
||||
const isDev = Boolean(manifest.devDependencies?.[depGraphNode.name])
|
||||
const isOptional = Boolean(manifest.optionalDependencies?.[depGraphNode.name])
|
||||
if (
|
||||
isDev && !opts.include.devDependencies ||
|
||||
isOptional && !opts.include.optionalDependencies ||
|
||||
!isDev && !isOptional && !opts.include.dependencies
|
||||
) return
|
||||
if (
|
||||
(await symlinkDependency(depGraphNode.dir, modulesDir, rootAlias)).reused
|
||||
) return
|
||||
if (opts.symlink) {
|
||||
await Promise.all(projects.map(async ({ id, manifest, modulesDir, rootDir }) => {
|
||||
const deps = opts.dependenciesByProjectId[id]
|
||||
await Promise.all([
|
||||
...Object.entries(deps)
|
||||
.map(([rootAlias, depPath]) => ({ rootAlias, depGraphNode: depGraph[depPath] }))
|
||||
.filter(({ depGraphNode }) => depGraphNode)
|
||||
.map(async ({ rootAlias, depGraphNode }) => {
|
||||
const isDev = Boolean(manifest.devDependencies?.[depGraphNode.name])
|
||||
const isOptional = Boolean(manifest.optionalDependencies?.[depGraphNode.name])
|
||||
if (
|
||||
isDev && !opts.include.devDependencies ||
|
||||
isOptional && !opts.include.optionalDependencies ||
|
||||
!isDev && !isOptional && !opts.include.dependencies
|
||||
) return
|
||||
if (
|
||||
(await symlinkDependency(depGraphNode.dir, modulesDir, rootAlias)).reused
|
||||
) return
|
||||
|
||||
rootLogger.debug({
|
||||
added: {
|
||||
dependencyType: isDev && 'dev' || isOptional && 'optional' || 'prod',
|
||||
id: depGraphNode.id,
|
||||
latest: opts.outdatedDependencies[depGraphNode.id],
|
||||
name: rootAlias,
|
||||
realName: depGraphNode.name,
|
||||
version: depGraphNode.version,
|
||||
},
|
||||
rootLogger.debug({
|
||||
added: {
|
||||
dependencyType: isDev && 'dev' || isOptional && 'optional' || 'prod',
|
||||
id: depGraphNode.id,
|
||||
latest: opts.outdatedDependencies[depGraphNode.id],
|
||||
name: rootAlias,
|
||||
realName: depGraphNode.name,
|
||||
version: depGraphNode.version,
|
||||
},
|
||||
prefix: rootDir,
|
||||
})
|
||||
}),
|
||||
...opts.linkedDependenciesByProjectId[id].map((linkedDependency) => {
|
||||
const depLocation = resolvePath(rootDir, linkedDependency.resolution.directory)
|
||||
return symlinkDirectRootDependency(depLocation, modulesDir, linkedDependency.alias, {
|
||||
fromDependenciesField: linkedDependency.dev && 'devDependencies' || linkedDependency.optional && 'optionalDependencies' || 'dependencies',
|
||||
linkedPackage: linkedDependency,
|
||||
prefix: rootDir,
|
||||
})
|
||||
}),
|
||||
...opts.linkedDependenciesByProjectId[id].map((linkedDependency) => {
|
||||
const depLocation = resolvePath(rootDir, linkedDependency.resolution.directory)
|
||||
return symlinkDirectRootDependency(depLocation, modulesDir, linkedDependency.alias, {
|
||||
fromDependenciesField: linkedDependency.dev && 'devDependencies' || linkedDependency.optional && 'optionalDependencies' || 'dependencies',
|
||||
linkedPackage: linkedDependency,
|
||||
prefix: rootDir,
|
||||
})
|
||||
}),
|
||||
])
|
||||
}))
|
||||
])
|
||||
}))
|
||||
}
|
||||
|
||||
let currentLockfile: Lockfile
|
||||
const allImportersIncluded = R.equals(projectIds.sort(), Object.keys(opts.wantedLockfile.importers).sort())
|
||||
@@ -265,6 +269,7 @@ async function linkNewPackages (
|
||||
optional: boolean
|
||||
lockfileDir: string
|
||||
sideEffectsCacheRead: boolean
|
||||
symlink: boolean
|
||||
skipped: Set<string>
|
||||
storeController: StoreController
|
||||
virtualStoreDir: string
|
||||
@@ -314,14 +319,12 @@ async function linkNewPackages (
|
||||
|
||||
await Promise.all(newPkgs.map((depNode) => fs.mkdir(depNode.modules, { recursive: true })))
|
||||
await Promise.all([
|
||||
linkAllModules(newPkgs, depGraph, {
|
||||
lockfileDir: opts.lockfileDir,
|
||||
optional: opts.optional,
|
||||
}),
|
||||
linkAllModules(existingWithUpdatedDeps, depGraph, {
|
||||
lockfileDir: opts.lockfileDir,
|
||||
optional: opts.optional,
|
||||
}),
|
||||
!opts.symlink
|
||||
? Promise.resolve()
|
||||
: linkAllModules([...newPkgs, ...existingWithUpdatedDeps], depGraph, {
|
||||
lockfileDir: opts.lockfileDir,
|
||||
optional: opts.optional,
|
||||
}),
|
||||
linkAllPkgs(opts.storeController, newPkgs, {
|
||||
force: opts.force,
|
||||
lockfileDir: opts.lockfileDir,
|
||||
@@ -396,7 +399,7 @@ function linkAllPkgs (
|
||||
)
|
||||
}
|
||||
|
||||
function linkAllModules (
|
||||
async function linkAllModules (
|
||||
depNodes: DependenciesGraphNode[],
|
||||
depGraph: DependenciesGraph,
|
||||
opts: {
|
||||
@@ -404,7 +407,7 @@ function linkAllModules (
|
||||
optional: boolean
|
||||
}
|
||||
) {
|
||||
return Promise.all(
|
||||
await Promise.all(
|
||||
depNodes
|
||||
.map(async ({ children, optionalDependencies, name, modules }) => {
|
||||
const childrenToLink = opts.optional
|
||||
|
||||
@@ -1227,3 +1227,24 @@ test('memory consumption is under control on huge package with many peer depende
|
||||
|
||||
t.ok(await exists('pnpm-lock.yaml'), 'lockfile created')
|
||||
})
|
||||
|
||||
test('installing with no symlinks', async (t) => {
|
||||
const project = prepareEmpty(t)
|
||||
|
||||
await addDependenciesToPackage(
|
||||
{
|
||||
name: 'project',
|
||||
version: '0.0.0',
|
||||
},
|
||||
['rimraf@2.7.1'],
|
||||
await testDefaults({ symlink: false })
|
||||
)
|
||||
|
||||
t.deepEqual(await fs.readdir(path.resolve('node_modules')), ['.modules.yaml', '.pnpm'])
|
||||
t.deepEqual(await fs.readdir(path.resolve('node_modules/.pnpm/rimraf@2.7.1/node_modules')), ['rimraf'])
|
||||
|
||||
t.ok(await project.readCurrentLockfile())
|
||||
t.ok(await project.readModulesManifest())
|
||||
|
||||
t.end()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user