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:
Zoltan Kochan
2020-09-28 21:46:02 +03:00
committed by GitHub
parent c3d34232c5
commit 74914c1784
9 changed files with 127 additions and 61 deletions

View File

@@ -0,0 +1,7 @@
---
"@pnpm/config": minor
"@pnpm/headless": minor
"supi": patch
---
New experimental option added for installing node_modules w/o symlinks.

View File

@@ -113,6 +113,7 @@ export interface Config {
gitChecks?: boolean
publishBranch?: string
recursiveInstall?: boolean
symlink: boolean
registries: Registries
ignoreWorkspaceRootCheck: boolean

View File

@@ -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:

View File

@@ -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

View File

@@ -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()
})

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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()
})