pnpm

Форк
0
936 строк · 31.4 Кб
1
import { promises as fs } from 'fs'
2
import path from 'path'
3
import { buildModules } from '@pnpm/build-modules'
4
import { createAllowBuildFunction } from '@pnpm/builder.policy'
5
import { calcDepState, type DepsStateCache } from '@pnpm/calc-dep-state'
6
import {
7
  LAYOUT_VERSION,
8
  WANTED_LOCKFILE,
9
} from '@pnpm/constants'
10
import {
11
  packageManifestLogger,
12
  progressLogger,
13
  stageLogger,
14
  statsLogger,
15
  summaryLogger,
16
} from '@pnpm/core-loggers'
17
import {
18
  filterLockfileByEngine,
19
  filterLockfileByImportersAndEngine,
20
} from '@pnpm/filter-lockfile'
21
import { hoist, type HoistedWorkspaceProject } from '@pnpm/hoist'
22
import {
23
  runLifecycleHooksConcurrently,
24
  makeNodeRequireOption,
25
} from '@pnpm/lifecycle'
26
import { linkBins, linkBinsOfPackages } from '@pnpm/link-bins'
27
import {
28
  getLockfileImporterId,
29
  type Lockfile,
30
  readCurrentLockfile,
31
  readWantedLockfile,
32
  writeLockfiles,
33
  writeCurrentLockfile,
34
  type PatchFile,
35
} from '@pnpm/lockfile-file'
36
import { writePnpFile } from '@pnpm/lockfile-to-pnp'
37
import {
38
  extendProjectsWithTargetDirs,
39
  nameVerFromPkgSnapshot,
40
} from '@pnpm/lockfile-utils'
41
import {
42
  type LogBase,
43
  logger,
44
  streamParser,
45
} from '@pnpm/logger'
46
import { prune } from '@pnpm/modules-cleaner'
47
import {
48
  type IncludedDependencies,
49
  writeModulesManifest,
50
} from '@pnpm/modules-yaml'
51
import { type HoistingLimits } from '@pnpm/real-hoist'
52
import { readPackageJsonFromDir } from '@pnpm/read-package-json'
53
import { readProjectManifestOnly, safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
54
import {
55
  type PackageFilesResponse,
56
  type StoreController,
57
} from '@pnpm/store-controller-types'
58
import { symlinkDependency } from '@pnpm/symlink-dependency'
59
import { type DependencyManifest, type HoistedDependencies, type ProjectManifest, type Registries, DEPENDENCIES_FIELDS, type SupportedArchitectures } from '@pnpm/types'
60
import * as dp from '@pnpm/dependency-path'
61
import { symlinkAllModules } from '@pnpm/worker'
62
import pLimit from 'p-limit'
63
import pathAbsolute from 'path-absolute'
64
import equals from 'ramda/src/equals'
65
import isEmpty from 'ramda/src/isEmpty'
66
import omit from 'ramda/src/omit'
67
import pick from 'ramda/src/pick'
68
import pickBy from 'ramda/src/pickBy'
69
import props from 'ramda/src/props'
70
import union from 'ramda/src/union'
71
import realpathMissing from 'realpath-missing'
72
import { linkHoistedModules } from './linkHoistedModules'
73
import {
74
  type DirectDependenciesByImporterId,
75
  type DependenciesGraph,
76
  type DependenciesGraphNode,
77
  type LockfileToDepGraphOptions,
78
  lockfileToDepGraph,
79
} from '@pnpm/deps.graph-builder'
80
import { lockfileToHoistedDepGraph } from './lockfileToHoistedDepGraph'
81
import { linkDirectDeps, type LinkedDirectDep } from '@pnpm/pkg-manager.direct-dep-linker'
82

83
export type { HoistingLimits }
84

85
export type ReporterFunction = (logObj: LogBase) => void
86

87
export interface Project {
88
  binsDir: string
89
  buildIndex: number
90
  manifest: ProjectManifest
91
  modulesDir: string
92
  id: string
93
  pruneDirectDependencies?: boolean
94
  rootDir: string
95
}
96

97
export interface HeadlessOptions {
98
  neverBuiltDependencies?: string[]
99
  onlyBuiltDependencies?: string[]
100
  onlyBuiltDependenciesFile?: string
101
  autoInstallPeers?: boolean
102
  childConcurrency?: number
103
  currentLockfile?: Lockfile
104
  currentEngine: {
105
    nodeVersion?: string
106
    pnpmVersion: string
107
  }
108
  dedupeDirectDeps?: boolean
109
  enablePnp?: boolean
110
  engineStrict: boolean
111
  excludeLinksFromLockfile?: boolean
112
  extraBinPaths?: string[]
113
  extraEnv?: Record<string, string>
114
  extraNodePaths?: string[]
115
  preferSymlinkedExecutables?: boolean
116
  hoistingLimits?: HoistingLimits
117
  externalDependencies?: Set<string>
118
  ignoreDepScripts: boolean
119
  ignoreScripts: boolean
120
  ignorePackageManifest?: boolean
121
  include: IncludedDependencies
122
  selectedProjectDirs: string[]
123
  allProjects: Record<string, Project>
124
  prunedAt?: string
125
  hoistedDependencies: HoistedDependencies
126
  hoistPattern?: string[]
127
  publicHoistPattern?: string[]
128
  currentHoistedLocations?: Record<string, string[]>
129
  lockfileDir: string
130
  modulesDir?: string
131
  virtualStoreDir?: string
132
  patchedDependencies?: Record<string, PatchFile>
133
  scriptsPrependNodePath?: boolean | 'warn-only'
134
  scriptShell?: string
135
  shellEmulator?: boolean
136
  storeController: StoreController
137
  sideEffectsCacheRead: boolean
138
  sideEffectsCacheWrite: boolean
139
  symlink?: boolean
140
  disableRelinkLocalDirDeps?: boolean
141
  force: boolean
142
  storeDir: string
143
  rawConfig: object
144
  unsafePerm: boolean
145
  userAgent: string
146
  registries: Registries
147
  reporter?: ReporterFunction
148
  packageManager: {
149
    name: string
150
    version: string
151
  }
152
  pruneStore: boolean
153
  pruneVirtualStore?: boolean
154
  wantedLockfile?: Lockfile
155
  ownLifecycleHooksStdio?: 'inherit' | 'pipe'
156
  pendingBuilds: string[]
157
  resolveSymlinksInInjectedDirs?: boolean
158
  skipped: Set<string>
159
  enableModulesDir?: boolean
160
  nodeLinker?: 'isolated' | 'hoisted' | 'pnp'
161
  useGitBranchLockfile?: boolean
162
  useLockfile?: boolean
163
  supportedArchitectures?: SupportedArchitectures
164
  hoistWorkspacePackages?: boolean
165
}
166

167
export interface InstallationResultStats {
168
  added: number
169
  removed: number
170
  linkedToRoot: number
171
}
172

173
export interface InstallationResult {
174
  stats: InstallationResultStats
175
}
176

177
export async function headlessInstall (opts: HeadlessOptions): Promise<InstallationResult> {
178
  const reporter = opts.reporter
179
  if ((reporter != null) && typeof reporter === 'function') {
180
    streamParser.on('data', reporter)
181
  }
182

183
  const lockfileDir = opts.lockfileDir
184
  const wantedLockfile = opts.wantedLockfile ?? await readWantedLockfile(lockfileDir, {
185
    ignoreIncompatible: false,
186
    useGitBranchLockfile: opts.useGitBranchLockfile,
187
    // mergeGitBranchLockfiles is intentionally not supported in headless
188
    mergeGitBranchLockfiles: false,
189
  })
190

191
  if (wantedLockfile == null) {
192
    throw new Error(`Headless installation requires a ${WANTED_LOCKFILE} file`)
193
  }
194

195
  const depsStateCache: DepsStateCache = {}
196
  const relativeModulesDir = opts.modulesDir ?? 'node_modules'
197
  const rootModulesDir = await realpathMissing(path.join(lockfileDir, relativeModulesDir))
198
  const virtualStoreDir = pathAbsolute(opts.virtualStoreDir ?? path.join(relativeModulesDir, '.pnpm'), lockfileDir)
199
  const currentLockfile = opts.currentLockfile ?? await readCurrentLockfile(virtualStoreDir, { ignoreIncompatible: false })
200
  const hoistedModulesDir = path.join(virtualStoreDir, 'node_modules')
201
  const publicHoistedModulesDir = rootModulesDir
202
  const selectedProjects = Object.values(pick(opts.selectedProjectDirs, opts.allProjects))
203

204
  const scriptsOpts = {
205
    optional: false,
206
    extraBinPaths: opts.extraBinPaths,
207
    extraNodePaths: opts.extraNodePaths,
208
    preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
209
    extraEnv: opts.extraEnv,
210
    rawConfig: opts.rawConfig,
211
    resolveSymlinksInInjectedDirs: opts.resolveSymlinksInInjectedDirs,
212
    scriptsPrependNodePath: opts.scriptsPrependNodePath,
213
    scriptShell: opts.scriptShell,
214
    shellEmulator: opts.shellEmulator,
215
    stdio: opts.ownLifecycleHooksStdio ?? 'inherit',
216
    storeController: opts.storeController,
217
    unsafePerm: opts.unsafePerm || false,
218
  }
219

220
  const skipped = opts.skipped || new Set<string>()
221
  const filterOpts = {
222
    include: opts.include,
223
    registries: opts.registries,
224
    skipped,
225
    currentEngine: opts.currentEngine,
226
    engineStrict: opts.engineStrict,
227
    failOnMissingDependencies: true,
228
    includeIncompatiblePackages: opts.force,
229
    lockfileDir,
230
    supportedArchitectures: opts.supportedArchitectures,
231
  }
232
  let removed = 0
233
  if (opts.nodeLinker !== 'hoisted') {
234
    if (currentLockfile != null && !opts.ignorePackageManifest) {
235
      const removedDepPaths = await prune(
236
        selectedProjects,
237
        {
238
          currentLockfile,
239
          dedupeDirectDeps: opts.dedupeDirectDeps,
240
          dryRun: false,
241
          hoistedDependencies: opts.hoistedDependencies,
242
          hoistedModulesDir: (opts.hoistPattern == null) ? undefined : hoistedModulesDir,
243
          include: opts.include,
244
          lockfileDir,
245
          pruneStore: opts.pruneStore,
246
          pruneVirtualStore: opts.pruneVirtualStore,
247
          publicHoistedModulesDir: (opts.publicHoistPattern == null) ? undefined : publicHoistedModulesDir,
248
          skipped,
249
          storeController: opts.storeController,
250
          virtualStoreDir,
251
          wantedLockfile: filterLockfileByEngine(wantedLockfile, filterOpts).lockfile,
252
        }
253
      )
254
      removed = removedDepPaths.size
255
    } else {
256
      statsLogger.debug({
257
        prefix: lockfileDir,
258
        removed: 0,
259
      })
260
    }
261
  }
262

263
  stageLogger.debug({
264
    prefix: lockfileDir,
265
    stage: 'importing_started',
266
  })
267

268
  const initialImporterIds = (opts.ignorePackageManifest === true || opts.nodeLinker === 'hoisted')
269
    ? Object.keys(wantedLockfile.importers)
270
    : selectedProjects.map(({ id }) => id)
271
  const { lockfile: filteredLockfile, selectedImporterIds: importerIds } = filterLockfileByImportersAndEngine(wantedLockfile, initialImporterIds, filterOpts)
272
  if (opts.excludeLinksFromLockfile) {
273
    for (const { id, manifest, rootDir } of selectedProjects) {
274
      if (filteredLockfile.importers[id]) {
275
        for (const depType of DEPENDENCIES_FIELDS) {
276
          filteredLockfile.importers[id][depType] = {
277
            ...filteredLockfile.importers[id][depType],
278
            ...Object.entries(manifest[depType] ?? {})
279
              .filter(([_, spec]) => spec.startsWith('link:'))
280
              .reduce((acc, [depName, spec]) => {
281
                const linkPath = spec.substring(5)
282
                acc[depName] = path.isAbsolute(linkPath) ? `link:${path.relative(rootDir, spec.substring(5))}` : spec
283
                return acc
284
              }, {} as Record<string, string>),
285
          }
286
        }
287
      }
288
    }
289
  }
290

291
  // Update selectedProjects to add missing projects. importerIds will have the updated ids, found from deeply linked workspace projects
292
  const initialImporterIdSet = new Set(initialImporterIds)
293
  const missingIds = importerIds.filter((importerId) => !initialImporterIdSet.has(importerId))
294
  if (missingIds.length > 0) {
295
    for (const project of Object.values(opts.allProjects)) {
296
      if (missingIds.includes(project.id)) {
297
        selectedProjects.push(project)
298
      }
299
    }
300
  }
301

302
  const lockfileToDepGraphOpts = {
303
    ...opts,
304
    importerIds,
305
    lockfileDir,
306
    skipped,
307
    virtualStoreDir,
308
    nodeVersion: opts.currentEngine.nodeVersion,
309
    pnpmVersion: opts.currentEngine.pnpmVersion,
310
    supportedArchitectures: opts.supportedArchitectures,
311
  } as LockfileToDepGraphOptions
312
  const {
313
    directDependenciesByImporterId,
314
    graph,
315
    hierarchy,
316
    hoistedLocations,
317
    pkgLocationsByDepPath,
318
    prevGraph,
319
    symlinkedDirectDependenciesByImporterId,
320
  } = await (
321
    opts.nodeLinker === 'hoisted'
322
      ? lockfileToHoistedDepGraph(
323
        filteredLockfile,
324
        currentLockfile,
325
        lockfileToDepGraphOpts
326
      )
327
      : lockfileToDepGraph(
328
        filteredLockfile,
329
        opts.force ? null : currentLockfile,
330
        lockfileToDepGraphOpts
331
      )
332
  )
333
  if (opts.enablePnp) {
334
    const importerNames = Object.fromEntries(
335
      selectedProjects.map(({ manifest, id }) => [id, manifest.name ?? id])
336
    )
337
    await writePnpFile(filteredLockfile, {
338
      importerNames,
339
      lockfileDir,
340
      virtualStoreDir,
341
      registries: opts.registries,
342
    })
343
  }
344
  const depNodes = Object.values(graph)
345

346
  const added = depNodes.filter(({ fetching }) => fetching).length
347
  statsLogger.debug({
348
    added,
349
    prefix: lockfileDir,
350
  })
351

352
  function warn (message: string) {
353
    logger.info({
354
      message,
355
      prefix: lockfileDir,
356
    })
357
  }
358

359
  let newHoistedDependencies!: HoistedDependencies
360
  let linkedToRoot = 0
361
  if (opts.nodeLinker === 'hoisted' && hierarchy && prevGraph) {
362
    await linkHoistedModules(opts.storeController, graph, prevGraph, hierarchy, {
363
      depsStateCache,
364
      disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps,
365
      force: opts.force,
366
      ignoreScripts: opts.ignoreScripts,
367
      lockfileDir: opts.lockfileDir,
368
      preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
369
      sideEffectsCacheRead: opts.sideEffectsCacheRead,
370
    })
371
    stageLogger.debug({
372
      prefix: lockfileDir,
373
      stage: 'importing_done',
374
    })
375

376
    linkedToRoot = await symlinkDirectDependencies({
377
      directDependenciesByImporterId: symlinkedDirectDependenciesByImporterId!,
378
      dedupe: Boolean(opts.dedupeDirectDeps),
379
      filteredLockfile,
380
      lockfileDir,
381
      projects: selectedProjects,
382
      registries: opts.registries,
383
      symlink: opts.symlink,
384
    })
385
  } else if (opts.enableModulesDir !== false) {
386
    await Promise.all(depNodes.map(async (depNode) => fs.mkdir(depNode.modules, { recursive: true })))
387
    await Promise.all([
388
      opts.symlink === false
389
        ? Promise.resolve()
390
        : linkAllModules(depNodes, {
391
          optional: opts.include.optionalDependencies,
392
        }),
393
      linkAllPkgs(opts.storeController, depNodes, {
394
        force: opts.force,
395
        disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps,
396
        depGraph: graph,
397
        depsStateCache,
398
        ignoreScripts: opts.ignoreScripts,
399
        lockfileDir: opts.lockfileDir,
400
        sideEffectsCacheRead: opts.sideEffectsCacheRead,
401
      }),
402
    ])
403

404
    stageLogger.debug({
405
      prefix: lockfileDir,
406
      stage: 'importing_done',
407
    })
408

409
    if (opts.ignorePackageManifest !== true && (opts.hoistPattern != null || opts.publicHoistPattern != null)) {
410
      // It is important to keep the skipped packages in the lockfile which will be saved as the "current lockfile".
411
      // pnpm is comparing the current lockfile to the wanted one and they should match.
412
      // But for hoisting, we need a version of the lockfile w/o the skipped packages, so we're making a copy.
413
      const hoistLockfile = {
414
        ...filteredLockfile,
415
        packages: filteredLockfile.packages != null ? omit(Array.from(skipped), filteredLockfile.packages) : {},
416
      }
417
      newHoistedDependencies = await hoist({
418
        extraNodePath: opts.extraNodePaths,
419
        lockfile: hoistLockfile,
420
        importerIds,
421
        preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
422
        privateHoistedModulesDir: hoistedModulesDir,
423
        privateHoistPattern: opts.hoistPattern ?? [],
424
        publicHoistedModulesDir,
425
        publicHoistPattern: opts.publicHoistPattern ?? [],
426
        virtualStoreDir,
427
        hoistedWorkspacePackages: opts.hoistWorkspacePackages
428
          ? Object.values(opts.allProjects).reduce((hoistedWorkspacePackages, project) => {
429
            if (project.manifest.name && project.id !== '.') {
430
              hoistedWorkspacePackages[project.id] = {
431
                dir: project.rootDir,
432
                name: project.manifest.name,
433
              }
434
            }
435
            return hoistedWorkspacePackages
436
          }, {} as Record<string, HoistedWorkspaceProject>)
437
          : undefined,
438
      })
439
    } else {
440
      newHoistedDependencies = {}
441
    }
442

443
    await linkAllBins(graph, {
444
      extraNodePaths: opts.extraNodePaths,
445
      optional: opts.include.optionalDependencies,
446
      preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
447
      warn,
448
    })
449

450
    if ((currentLockfile != null) && !equals(importerIds.sort(), Object.keys(filteredLockfile.importers).sort())) {
451
      Object.assign(filteredLockfile.packages!, currentLockfile.packages)
452
    }
453

454
    /** Skip linking and due to no project manifest */
455
    if (!opts.ignorePackageManifest) {
456
      linkedToRoot = await symlinkDirectDependencies({
457
        dedupe: Boolean(opts.dedupeDirectDeps),
458
        directDependenciesByImporterId,
459
        filteredLockfile,
460
        lockfileDir,
461
        projects: selectedProjects,
462
        registries: opts.registries,
463
        symlink: opts.symlink,
464
      })
465
    }
466
  }
467

468
  if (opts.ignoreScripts) {
469
    for (const { id, manifest } of selectedProjects) {
470
      if (opts.ignoreScripts && ((manifest?.scripts) != null) &&
471
        (manifest.scripts.preinstall ?? manifest.scripts.prepublish ??
472
          manifest.scripts.install ??
473
          manifest.scripts.postinstall ??
474
          manifest.scripts.prepare)
475
      ) {
476
        opts.pendingBuilds.push(id)
477
      }
478
    }
479
    // we can use concat here because we always only append new packages, which are guaranteed to not be there by definition
480
    opts.pendingBuilds = opts.pendingBuilds
481
      .concat(
482
        depNodes
483
          .filter(({ requiresBuild }) => requiresBuild)
484
          .map(({ depPath }) => depPath)
485
      )
486
  }
487
  if (!opts.ignoreScripts || Object.keys(opts.patchedDependencies ?? {}).length > 0) {
488
    const directNodes = new Set<string>()
489
    for (const id of union(importerIds, ['.'])) {
490
      Object
491
        .values(directDependenciesByImporterId[id] ?? {})
492
        .filter((loc) => graph[loc])
493
        .forEach((loc) => {
494
          directNodes.add(loc)
495
        })
496
    }
497
    const extraBinPaths = [...opts.extraBinPaths ?? []]
498
    if (opts.hoistPattern != null) {
499
      extraBinPaths.unshift(path.join(virtualStoreDir, 'node_modules/.bin'))
500
    }
501
    let extraEnv: Record<string, string> | undefined = opts.extraEnv
502
    if (opts.enablePnp) {
503
      extraEnv = {
504
        ...extraEnv,
505
        ...makeNodeRequireOption(path.join(opts.lockfileDir, '.pnp.cjs')),
506
      }
507
    }
508
    await buildModules(graph, Array.from(directNodes), {
509
      allowBuild: createAllowBuildFunction(opts),
510
      childConcurrency: opts.childConcurrency,
511
      extraBinPaths,
512
      extraEnv,
513
      depsStateCache,
514
      ignoreScripts: opts.ignoreScripts || opts.ignoreDepScripts,
515
      hoistedLocations,
516
      lockfileDir,
517
      optional: opts.include.optionalDependencies,
518
      preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
519
      rawConfig: opts.rawConfig,
520
      rootModulesDir: virtualStoreDir,
521
      scriptsPrependNodePath: opts.scriptsPrependNodePath,
522
      scriptShell: opts.scriptShell,
523
      shellEmulator: opts.shellEmulator,
524
      sideEffectsCacheWrite: opts.sideEffectsCacheWrite,
525
      storeController: opts.storeController,
526
      unsafePerm: opts.unsafePerm,
527
      userAgent: opts.userAgent,
528
    })
529
  }
530

531
  const projectsToBeBuilt = extendProjectsWithTargetDirs(selectedProjects, wantedLockfile, {
532
    pkgLocationsByDepPath,
533
    virtualStoreDir,
534
  })
535

536
  if (opts.enableModulesDir !== false) {
537
    const rootProjectDeps = !opts.dedupeDirectDeps ? {} : (directDependenciesByImporterId['.'] ?? {})
538
    /** Skip linking and due to no project manifest */
539
    if (!opts.ignorePackageManifest) {
540
      await Promise.all(selectedProjects.map(async (project) => {
541
        if (opts.nodeLinker === 'hoisted' || opts.publicHoistPattern?.length && path.relative(opts.lockfileDir, project.rootDir) === '') {
542
          await linkBinsOfImporter(project, {
543
            extraNodePaths: opts.extraNodePaths,
544
            preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
545
          })
546
        } else {
547
          let directPkgDirs: string[]
548
          if (project.id === '.') {
549
            directPkgDirs = Object.values(directDependenciesByImporterId[project.id])
550
          } else {
551
            directPkgDirs = []
552
            for (const [alias, dir] of Object.entries(directDependenciesByImporterId[project.id])) {
553
              if (rootProjectDeps[alias] !== dir) {
554
                directPkgDirs.push(dir)
555
              }
556
            }
557
          }
558
          await linkBinsOfPackages(
559
            (
560
              await Promise.all(
561
                directPkgDirs.map(async (dir) => ({
562
                  location: dir,
563
                  manifest: await safeReadProjectManifestOnly(dir),
564
                }))
565
              )
566
            )
567
              .filter(({ manifest }) => manifest != null) as Array<{ location: string, manifest: DependencyManifest }>,
568
            project.binsDir,
569
            {
570
              extraNodePaths: opts.extraNodePaths,
571
              preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
572
            }
573
          )
574
        }
575
      }))
576
    }
577
    const injectedDeps: Record<string, string[]> = {}
578
    for (const project of projectsToBeBuilt) {
579
      if (project.targetDirs.length > 0) {
580
        injectedDeps[project.id] = project.targetDirs.map((targetDir) => path.relative(opts.lockfileDir, targetDir))
581
      }
582
    }
583
    await writeModulesManifest(rootModulesDir, {
584
      hoistedDependencies: newHoistedDependencies,
585
      hoistPattern: opts.hoistPattern,
586
      included: opts.include,
587
      injectedDeps,
588
      layoutVersion: LAYOUT_VERSION,
589
      hoistedLocations,
590
      nodeLinker: opts.nodeLinker,
591
      packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`,
592
      pendingBuilds: opts.pendingBuilds,
593
      publicHoistPattern: opts.publicHoistPattern,
594
      prunedAt: opts.pruneVirtualStore === true || opts.prunedAt == null
595
        ? new Date().toUTCString()
596
        : opts.prunedAt,
597
      registries: opts.registries,
598
      skipped: Array.from(skipped),
599
      storeDir: opts.storeDir,
600
      virtualStoreDir,
601
    }, {
602
      makeModulesDir: Object.keys(filteredLockfile.packages ?? {}).length > 0,
603
    })
604
    if (opts.useLockfile) {
605
      // We need to write the wanted lockfile as well.
606
      // Even though it will only be changed if the workspace will have new projects with no dependencies.
607
      await writeLockfiles({
608
        wantedLockfileDir: opts.lockfileDir,
609
        currentLockfileDir: virtualStoreDir,
610
        wantedLockfile,
611
        currentLockfile: filteredLockfile,
612
      })
613
    } else {
614
      await writeCurrentLockfile(virtualStoreDir, filteredLockfile)
615
    }
616
  }
617

618
  // waiting till package requests are finished
619
  await Promise.all(depNodes.map(async ({ fetching }) => {
620
    try {
621
      await fetching?.()
622
    } catch {}
623
  }))
624

625
  summaryLogger.debug({ prefix: lockfileDir })
626

627
  await opts.storeController.close()
628

629
  if (!opts.ignoreScripts && !opts.ignorePackageManifest) {
630
    await runLifecycleHooksConcurrently(
631
      ['preinstall', 'install', 'postinstall', 'prepare'],
632
      projectsToBeBuilt,
633
      opts.childConcurrency ?? 5,
634
      scriptsOpts
635
    )
636
  }
637

638
  if ((reporter != null) && typeof reporter === 'function') {
639
    streamParser.removeListener('data', reporter)
640
  }
641
  return {
642
    stats: {
643
      added,
644
      removed,
645
      linkedToRoot,
646
    },
647
  }
648
}
649

650
type SymlinkDirectDependenciesOpts = Pick<HeadlessOptions, 'registries' | 'symlink' | 'lockfileDir'> & {
651
  filteredLockfile: Lockfile
652
  dedupe: boolean
653
  directDependenciesByImporterId: DirectDependenciesByImporterId
654
  projects: Project[]
655
}
656

657
async function symlinkDirectDependencies (
658
  {
659
    filteredLockfile,
660
    dedupe,
661
    directDependenciesByImporterId,
662
    lockfileDir,
663
    projects,
664
    registries,
665
    symlink,
666
  }: SymlinkDirectDependenciesOpts
667
): Promise<number> {
668
  projects.forEach(({ rootDir, manifest }) => {
669
    // Even though headless installation will never update the package.json
670
    // this needs to be logged because otherwise install summary won't be printed
671
    packageManifestLogger.debug({
672
      prefix: rootDir,
673
      updated: manifest,
674
    })
675
  })
676
  if (symlink === false) return 0
677
  const importerManifestsByImporterId = {} as { [id: string]: ProjectManifest }
678
  for (const { id, manifest } of projects) {
679
    importerManifestsByImporterId[id] = manifest
680
  }
681
  const projectsToLink = Object.fromEntries(await Promise.all(
682
    projects.map(async ({ rootDir, id, modulesDir }) => ([id, {
683
      dir: rootDir,
684
      modulesDir,
685
      dependencies: await getRootPackagesToLink(filteredLockfile, {
686
        importerId: id,
687
        importerModulesDir: modulesDir,
688
        lockfileDir,
689
        projectDir: rootDir,
690
        importerManifestsByImporterId,
691
        registries,
692
        rootDependencies: directDependenciesByImporterId[id],
693
      }),
694
    }]))
695
  ))
696
  const rootProject = projectsToLink['.']
697
  if (rootProject && dedupe) {
698
    const rootDeps = Object.fromEntries(rootProject.dependencies.map((dep: LinkedDirectDep) => [dep.alias, dep.dir]))
699
    for (const project of Object.values(omit(['.'], projectsToLink))) {
700
      project.dependencies = project.dependencies.filter((dep: LinkedDirectDep) => dep.dir !== rootDeps[dep.alias])
701
    }
702
  }
703
  return linkDirectDeps(projectsToLink, { dedupe: Boolean(dedupe) })
704
}
705

706
async function linkBinsOfImporter (
707
  { manifest, modulesDir, binsDir, rootDir }: {
708
    binsDir: string
709
    manifest: ProjectManifest
710
    modulesDir: string
711
    rootDir: string
712
  },
713
  { extraNodePaths, preferSymlinkedExecutables }: { extraNodePaths?: string[], preferSymlinkedExecutables?: boolean } = {}
714
): Promise<string[]> {
715
  const warn = (message: string) => {
716
    logger.info({ message, prefix: rootDir })
717
  }
718
  return linkBins(modulesDir, binsDir, {
719
    extraNodePaths,
720
    allowExoticManifests: true,
721
    preferSymlinkedExecutables,
722
    projectManifest: manifest,
723
    warn,
724
  })
725
}
726

727
async function getRootPackagesToLink (
728
  lockfile: Lockfile,
729
  opts: {
730
    registries: Registries
731
    projectDir: string
732
    importerId: string
733
    importerModulesDir: string
734
    importerManifestsByImporterId: { [id: string]: ProjectManifest }
735
    lockfileDir: string
736
    rootDependencies: { [alias: string]: string }
737
  }
738
): Promise<LinkedDirectDep[]> {
739
  const projectSnapshot = lockfile.importers[opts.importerId]
740
  const allDeps = {
741
    ...projectSnapshot.devDependencies,
742
    ...projectSnapshot.dependencies,
743
    ...projectSnapshot.optionalDependencies,
744
  }
745
  return (await Promise.all(
746
    Object.entries(allDeps)
747
      .map(async ([alias, ref]) => {
748
        if (ref.startsWith('link:')) {
749
          const isDev = Boolean(projectSnapshot.devDependencies?.[alias])
750
          const isOptional = Boolean(projectSnapshot.optionalDependencies?.[alias])
751
          const packageDir = path.join(opts.projectDir, ref.slice(5))
752
          const linkedPackage = await (async () => {
753
            const importerId = getLockfileImporterId(opts.lockfileDir, packageDir)
754
            if (opts.importerManifestsByImporterId[importerId]) {
755
              return opts.importerManifestsByImporterId[importerId]
756
            }
757
            try {
758
              // TODO: cover this case with a test
759
              return await readProjectManifestOnly(packageDir) as DependencyManifest
760
            } catch (err: any) { // eslint-disable-line
761
              if (err['code'] !== 'ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND') throw err
762
              return { name: alias, version: '0.0.0' }
763
            }
764
          })() as DependencyManifest
765
          return {
766
            alias,
767
            name: linkedPackage.name,
768
            version: linkedPackage.version,
769
            dir: packageDir,
770
            id: ref,
771
            isExternalLink: true,
772
            dependencyType: isDev && 'dev' ||
773
              isOptional && 'optional' ||
774
              'prod',
775
          }
776
        }
777
        const dir = opts.rootDependencies[alias]
778
        // Skipping linked packages
779
        if (!dir) {
780
          return
781
        }
782
        const isDev = Boolean(projectSnapshot.devDependencies?.[alias])
783
        const isOptional = Boolean(projectSnapshot.optionalDependencies?.[alias])
784

785
        const depPath = dp.refToRelative(ref, alias)
786
        if (depPath === null) return
787
        const pkgSnapshot = lockfile.packages?.[depPath]
788
        if (pkgSnapshot == null) return // this won't ever happen. Just making typescript happy
789
        const pkgId = pkgSnapshot.id ?? dp.refToRelative(ref, alias) ?? undefined
790
        const pkgInfo = nameVerFromPkgSnapshot(depPath, pkgSnapshot)
791
        return {
792
          alias,
793
          isExternalLink: false,
794
          name: pkgInfo.name,
795
          version: pkgInfo.version,
796
          dependencyType: isDev && 'dev' || isOptional && 'optional' || 'prod',
797
          dir,
798
          id: pkgId,
799
        }
800
      })
801
  ))
802
    .filter(Boolean) as LinkedDirectDep[]
803
}
804

805
const limitLinking = pLimit(16)
806

807
async function linkAllPkgs (
808
  storeController: StoreController,
809
  depNodes: DependenciesGraphNode[],
810
  opts: {
811
    depGraph: DependenciesGraph
812
    depsStateCache: DepsStateCache
813
    disableRelinkLocalDirDeps?: boolean
814
    force: boolean
815
    ignoreScripts: boolean
816
    lockfileDir: string
817
    sideEffectsCacheRead: boolean
818
  }
819
): Promise<void> {
820
  await Promise.all(
821
    depNodes.map(async (depNode) => {
822
      if (!depNode.fetching) return
823
      let filesResponse!: PackageFilesResponse
824
      try {
825
        filesResponse = (await depNode.fetching()).files
826
      } catch (err: any) { // eslint-disable-line
827
        if (depNode.optional) return
828
        throw err
829
      }
830

831
      depNode.requiresBuild = filesResponse.requiresBuild
832
      let sideEffectsCacheKey: string | undefined
833
      if (opts.sideEffectsCacheRead && filesResponse.sideEffects && !isEmpty(filesResponse.sideEffects)) {
834
        sideEffectsCacheKey = calcDepState(opts.depGraph, opts.depsStateCache, depNode.dir, {
835
          isBuilt: !opts.ignoreScripts && depNode.requiresBuild,
836
          patchFileHash: depNode.patchFile?.hash,
837
        })
838
      }
839
      const { importMethod, isBuilt } = await storeController.importPackage(depNode.dir, {
840
        filesResponse,
841
        force: opts.force,
842
        disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps,
843
        requiresBuild: depNode.patchFile != null || depNode.requiresBuild,
844
        sideEffectsCacheKey,
845
      })
846
      if (importMethod) {
847
        progressLogger.debug({
848
          method: importMethod,
849
          requester: opts.lockfileDir,
850
          status: 'imported',
851
          to: depNode.dir,
852
        })
853
      }
854
      depNode.isBuilt = isBuilt
855

856
      const selfDep = depNode.children[depNode.name]
857
      if (selfDep) {
858
        const pkg = opts.depGraph[selfDep]
859
        if (!pkg) return
860
        const targetModulesDir = path.join(depNode.modules, depNode.name, 'node_modules')
861
        await limitLinking(async () => symlinkDependency(pkg.dir, targetModulesDir, depNode.name))
862
      }
863
    })
864
  )
865
}
866

867
async function linkAllBins (
868
  depGraph: DependenciesGraph,
869
  opts: {
870
    extraNodePaths?: string[]
871
    optional: boolean
872
    preferSymlinkedExecutables?: boolean
873
    warn: (message: string) => void
874
  }
875
): Promise<void> {
876
  await Promise.all(
877
    Object.values(depGraph)
878
      .map(async (depNode) => limitLinking(async () => {
879
        const childrenToLink: Record<string, string> = opts.optional
880
          ? depNode.children
881
          : pickBy((_, childAlias) => !depNode.optionalDependencies.has(childAlias), depNode.children)
882

883
        const binPath = path.join(depNode.dir, 'node_modules/.bin')
884
        const pkgSnapshots = props<string, DependenciesGraphNode>(Object.values(childrenToLink), depGraph)
885

886
        if (pkgSnapshots.includes(undefined as any)) { // eslint-disable-line
887
          await linkBins(depNode.modules, binPath, {
888
            extraNodePaths: opts.extraNodePaths,
889
            preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
890
            warn: opts.warn,
891
          })
892
        } else {
893
          const pkgs = await Promise.all(
894
            pkgSnapshots
895
              .filter(({ hasBin }) => hasBin)
896
              .map(async ({ dir }) => ({
897
                location: dir,
898
                manifest: await readPackageJsonFromDir(dir) as DependencyManifest,
899
              }))
900
          )
901

902
          await linkBinsOfPackages(pkgs, binPath, {
903
            extraNodePaths: opts.extraNodePaths,
904
            preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
905
          })
906
        }
907

908
        // link also the bundled dependencies` bins
909
        if (depNode.hasBundledDependencies) {
910
          const bundledModules = path.join(depNode.dir, 'node_modules')
911
          await linkBins(bundledModules, binPath, {
912
            extraNodePaths: opts.extraNodePaths,
913
            preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
914
            warn: opts.warn,
915
          })
916
        }
917
      }))
918
  )
919
}
920

921
async function linkAllModules (
922
  depNodes: Array<Pick<DependenciesGraphNode, 'children' | 'optionalDependencies' | 'modules' | 'name'>>,
923
  opts: {
924
    optional: boolean
925
  }
926
): Promise<void> {
927
  await symlinkAllModules({
928
    deps: depNodes.map((depNode) => ({
929
      children: opts.optional
930
        ? depNode.children
931
        : pickBy((_, childAlias) => !depNode.optionalDependencies.has(childAlias), depNode.children),
932
      modules: depNode.modules,
933
      name: depNode.name,
934
    })),
935
  })
936
}
937

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.