pnpm
936 строк · 31.4 Кб
1import { promises as fs } from 'fs'
2import path from 'path'
3import { buildModules } from '@pnpm/build-modules'
4import { createAllowBuildFunction } from '@pnpm/builder.policy'
5import { calcDepState, type DepsStateCache } from '@pnpm/calc-dep-state'
6import {
7LAYOUT_VERSION,
8WANTED_LOCKFILE,
9} from '@pnpm/constants'
10import {
11packageManifestLogger,
12progressLogger,
13stageLogger,
14statsLogger,
15summaryLogger,
16} from '@pnpm/core-loggers'
17import {
18filterLockfileByEngine,
19filterLockfileByImportersAndEngine,
20} from '@pnpm/filter-lockfile'
21import { hoist, type HoistedWorkspaceProject } from '@pnpm/hoist'
22import {
23runLifecycleHooksConcurrently,
24makeNodeRequireOption,
25} from '@pnpm/lifecycle'
26import { linkBins, linkBinsOfPackages } from '@pnpm/link-bins'
27import {
28getLockfileImporterId,
29type Lockfile,
30readCurrentLockfile,
31readWantedLockfile,
32writeLockfiles,
33writeCurrentLockfile,
34type PatchFile,
35} from '@pnpm/lockfile-file'
36import { writePnpFile } from '@pnpm/lockfile-to-pnp'
37import {
38extendProjectsWithTargetDirs,
39nameVerFromPkgSnapshot,
40} from '@pnpm/lockfile-utils'
41import {
42type LogBase,
43logger,
44streamParser,
45} from '@pnpm/logger'
46import { prune } from '@pnpm/modules-cleaner'
47import {
48type IncludedDependencies,
49writeModulesManifest,
50} from '@pnpm/modules-yaml'
51import { type HoistingLimits } from '@pnpm/real-hoist'
52import { readPackageJsonFromDir } from '@pnpm/read-package-json'
53import { readProjectManifestOnly, safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
54import {
55type PackageFilesResponse,
56type StoreController,
57} from '@pnpm/store-controller-types'
58import { symlinkDependency } from '@pnpm/symlink-dependency'
59import { type DependencyManifest, type HoistedDependencies, type ProjectManifest, type Registries, DEPENDENCIES_FIELDS, type SupportedArchitectures } from '@pnpm/types'
60import * as dp from '@pnpm/dependency-path'
61import { symlinkAllModules } from '@pnpm/worker'
62import pLimit from 'p-limit'
63import pathAbsolute from 'path-absolute'
64import equals from 'ramda/src/equals'
65import isEmpty from 'ramda/src/isEmpty'
66import omit from 'ramda/src/omit'
67import pick from 'ramda/src/pick'
68import pickBy from 'ramda/src/pickBy'
69import props from 'ramda/src/props'
70import union from 'ramda/src/union'
71import realpathMissing from 'realpath-missing'
72import { linkHoistedModules } from './linkHoistedModules'
73import {
74type DirectDependenciesByImporterId,
75type DependenciesGraph,
76type DependenciesGraphNode,
77type LockfileToDepGraphOptions,
78lockfileToDepGraph,
79} from '@pnpm/deps.graph-builder'
80import { lockfileToHoistedDepGraph } from './lockfileToHoistedDepGraph'
81import { linkDirectDeps, type LinkedDirectDep } from '@pnpm/pkg-manager.direct-dep-linker'
82
83export type { HoistingLimits }
84
85export type ReporterFunction = (logObj: LogBase) => void
86
87export interface Project {
88binsDir: string
89buildIndex: number
90manifest: ProjectManifest
91modulesDir: string
92id: string
93pruneDirectDependencies?: boolean
94rootDir: string
95}
96
97export interface HeadlessOptions {
98neverBuiltDependencies?: string[]
99onlyBuiltDependencies?: string[]
100onlyBuiltDependenciesFile?: string
101autoInstallPeers?: boolean
102childConcurrency?: number
103currentLockfile?: Lockfile
104currentEngine: {
105nodeVersion?: string
106pnpmVersion: string
107}
108dedupeDirectDeps?: boolean
109enablePnp?: boolean
110engineStrict: boolean
111excludeLinksFromLockfile?: boolean
112extraBinPaths?: string[]
113extraEnv?: Record<string, string>
114extraNodePaths?: string[]
115preferSymlinkedExecutables?: boolean
116hoistingLimits?: HoistingLimits
117externalDependencies?: Set<string>
118ignoreDepScripts: boolean
119ignoreScripts: boolean
120ignorePackageManifest?: boolean
121include: IncludedDependencies
122selectedProjectDirs: string[]
123allProjects: Record<string, Project>
124prunedAt?: string
125hoistedDependencies: HoistedDependencies
126hoistPattern?: string[]
127publicHoistPattern?: string[]
128currentHoistedLocations?: Record<string, string[]>
129lockfileDir: string
130modulesDir?: string
131virtualStoreDir?: string
132patchedDependencies?: Record<string, PatchFile>
133scriptsPrependNodePath?: boolean | 'warn-only'
134scriptShell?: string
135shellEmulator?: boolean
136storeController: StoreController
137sideEffectsCacheRead: boolean
138sideEffectsCacheWrite: boolean
139symlink?: boolean
140disableRelinkLocalDirDeps?: boolean
141force: boolean
142storeDir: string
143rawConfig: object
144unsafePerm: boolean
145userAgent: string
146registries: Registries
147reporter?: ReporterFunction
148packageManager: {
149name: string
150version: string
151}
152pruneStore: boolean
153pruneVirtualStore?: boolean
154wantedLockfile?: Lockfile
155ownLifecycleHooksStdio?: 'inherit' | 'pipe'
156pendingBuilds: string[]
157resolveSymlinksInInjectedDirs?: boolean
158skipped: Set<string>
159enableModulesDir?: boolean
160nodeLinker?: 'isolated' | 'hoisted' | 'pnp'
161useGitBranchLockfile?: boolean
162useLockfile?: boolean
163supportedArchitectures?: SupportedArchitectures
164hoistWorkspacePackages?: boolean
165}
166
167export interface InstallationResultStats {
168added: number
169removed: number
170linkedToRoot: number
171}
172
173export interface InstallationResult {
174stats: InstallationResultStats
175}
176
177export async function headlessInstall (opts: HeadlessOptions): Promise<InstallationResult> {
178const reporter = opts.reporter
179if ((reporter != null) && typeof reporter === 'function') {
180streamParser.on('data', reporter)
181}
182
183const lockfileDir = opts.lockfileDir
184const wantedLockfile = opts.wantedLockfile ?? await readWantedLockfile(lockfileDir, {
185ignoreIncompatible: false,
186useGitBranchLockfile: opts.useGitBranchLockfile,
187// mergeGitBranchLockfiles is intentionally not supported in headless
188mergeGitBranchLockfiles: false,
189})
190
191if (wantedLockfile == null) {
192throw new Error(`Headless installation requires a ${WANTED_LOCKFILE} file`)
193}
194
195const depsStateCache: DepsStateCache = {}
196const relativeModulesDir = opts.modulesDir ?? 'node_modules'
197const rootModulesDir = await realpathMissing(path.join(lockfileDir, relativeModulesDir))
198const virtualStoreDir = pathAbsolute(opts.virtualStoreDir ?? path.join(relativeModulesDir, '.pnpm'), lockfileDir)
199const currentLockfile = opts.currentLockfile ?? await readCurrentLockfile(virtualStoreDir, { ignoreIncompatible: false })
200const hoistedModulesDir = path.join(virtualStoreDir, 'node_modules')
201const publicHoistedModulesDir = rootModulesDir
202const selectedProjects = Object.values(pick(opts.selectedProjectDirs, opts.allProjects))
203
204const scriptsOpts = {
205optional: false,
206extraBinPaths: opts.extraBinPaths,
207extraNodePaths: opts.extraNodePaths,
208preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
209extraEnv: opts.extraEnv,
210rawConfig: opts.rawConfig,
211resolveSymlinksInInjectedDirs: opts.resolveSymlinksInInjectedDirs,
212scriptsPrependNodePath: opts.scriptsPrependNodePath,
213scriptShell: opts.scriptShell,
214shellEmulator: opts.shellEmulator,
215stdio: opts.ownLifecycleHooksStdio ?? 'inherit',
216storeController: opts.storeController,
217unsafePerm: opts.unsafePerm || false,
218}
219
220const skipped = opts.skipped || new Set<string>()
221const filterOpts = {
222include: opts.include,
223registries: opts.registries,
224skipped,
225currentEngine: opts.currentEngine,
226engineStrict: opts.engineStrict,
227failOnMissingDependencies: true,
228includeIncompatiblePackages: opts.force,
229lockfileDir,
230supportedArchitectures: opts.supportedArchitectures,
231}
232let removed = 0
233if (opts.nodeLinker !== 'hoisted') {
234if (currentLockfile != null && !opts.ignorePackageManifest) {
235const removedDepPaths = await prune(
236selectedProjects,
237{
238currentLockfile,
239dedupeDirectDeps: opts.dedupeDirectDeps,
240dryRun: false,
241hoistedDependencies: opts.hoistedDependencies,
242hoistedModulesDir: (opts.hoistPattern == null) ? undefined : hoistedModulesDir,
243include: opts.include,
244lockfileDir,
245pruneStore: opts.pruneStore,
246pruneVirtualStore: opts.pruneVirtualStore,
247publicHoistedModulesDir: (opts.publicHoistPattern == null) ? undefined : publicHoistedModulesDir,
248skipped,
249storeController: opts.storeController,
250virtualStoreDir,
251wantedLockfile: filterLockfileByEngine(wantedLockfile, filterOpts).lockfile,
252}
253)
254removed = removedDepPaths.size
255} else {
256statsLogger.debug({
257prefix: lockfileDir,
258removed: 0,
259})
260}
261}
262
263stageLogger.debug({
264prefix: lockfileDir,
265stage: 'importing_started',
266})
267
268const initialImporterIds = (opts.ignorePackageManifest === true || opts.nodeLinker === 'hoisted')
269? Object.keys(wantedLockfile.importers)
270: selectedProjects.map(({ id }) => id)
271const { lockfile: filteredLockfile, selectedImporterIds: importerIds } = filterLockfileByImportersAndEngine(wantedLockfile, initialImporterIds, filterOpts)
272if (opts.excludeLinksFromLockfile) {
273for (const { id, manifest, rootDir } of selectedProjects) {
274if (filteredLockfile.importers[id]) {
275for (const depType of DEPENDENCIES_FIELDS) {
276filteredLockfile.importers[id][depType] = {
277...filteredLockfile.importers[id][depType],
278...Object.entries(manifest[depType] ?? {})
279.filter(([_, spec]) => spec.startsWith('link:'))
280.reduce((acc, [depName, spec]) => {
281const linkPath = spec.substring(5)
282acc[depName] = path.isAbsolute(linkPath) ? `link:${path.relative(rootDir, spec.substring(5))}` : spec
283return 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
292const initialImporterIdSet = new Set(initialImporterIds)
293const missingIds = importerIds.filter((importerId) => !initialImporterIdSet.has(importerId))
294if (missingIds.length > 0) {
295for (const project of Object.values(opts.allProjects)) {
296if (missingIds.includes(project.id)) {
297selectedProjects.push(project)
298}
299}
300}
301
302const lockfileToDepGraphOpts = {
303...opts,
304importerIds,
305lockfileDir,
306skipped,
307virtualStoreDir,
308nodeVersion: opts.currentEngine.nodeVersion,
309pnpmVersion: opts.currentEngine.pnpmVersion,
310supportedArchitectures: opts.supportedArchitectures,
311} as LockfileToDepGraphOptions
312const {
313directDependenciesByImporterId,
314graph,
315hierarchy,
316hoistedLocations,
317pkgLocationsByDepPath,
318prevGraph,
319symlinkedDirectDependenciesByImporterId,
320} = await (
321opts.nodeLinker === 'hoisted'
322? lockfileToHoistedDepGraph(
323filteredLockfile,
324currentLockfile,
325lockfileToDepGraphOpts
326)
327: lockfileToDepGraph(
328filteredLockfile,
329opts.force ? null : currentLockfile,
330lockfileToDepGraphOpts
331)
332)
333if (opts.enablePnp) {
334const importerNames = Object.fromEntries(
335selectedProjects.map(({ manifest, id }) => [id, manifest.name ?? id])
336)
337await writePnpFile(filteredLockfile, {
338importerNames,
339lockfileDir,
340virtualStoreDir,
341registries: opts.registries,
342})
343}
344const depNodes = Object.values(graph)
345
346const added = depNodes.filter(({ fetching }) => fetching).length
347statsLogger.debug({
348added,
349prefix: lockfileDir,
350})
351
352function warn (message: string) {
353logger.info({
354message,
355prefix: lockfileDir,
356})
357}
358
359let newHoistedDependencies!: HoistedDependencies
360let linkedToRoot = 0
361if (opts.nodeLinker === 'hoisted' && hierarchy && prevGraph) {
362await linkHoistedModules(opts.storeController, graph, prevGraph, hierarchy, {
363depsStateCache,
364disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps,
365force: opts.force,
366ignoreScripts: opts.ignoreScripts,
367lockfileDir: opts.lockfileDir,
368preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
369sideEffectsCacheRead: opts.sideEffectsCacheRead,
370})
371stageLogger.debug({
372prefix: lockfileDir,
373stage: 'importing_done',
374})
375
376linkedToRoot = await symlinkDirectDependencies({
377directDependenciesByImporterId: symlinkedDirectDependenciesByImporterId!,
378dedupe: Boolean(opts.dedupeDirectDeps),
379filteredLockfile,
380lockfileDir,
381projects: selectedProjects,
382registries: opts.registries,
383symlink: opts.symlink,
384})
385} else if (opts.enableModulesDir !== false) {
386await Promise.all(depNodes.map(async (depNode) => fs.mkdir(depNode.modules, { recursive: true })))
387await Promise.all([
388opts.symlink === false
389? Promise.resolve()
390: linkAllModules(depNodes, {
391optional: opts.include.optionalDependencies,
392}),
393linkAllPkgs(opts.storeController, depNodes, {
394force: opts.force,
395disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps,
396depGraph: graph,
397depsStateCache,
398ignoreScripts: opts.ignoreScripts,
399lockfileDir: opts.lockfileDir,
400sideEffectsCacheRead: opts.sideEffectsCacheRead,
401}),
402])
403
404stageLogger.debug({
405prefix: lockfileDir,
406stage: 'importing_done',
407})
408
409if (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.
413const hoistLockfile = {
414...filteredLockfile,
415packages: filteredLockfile.packages != null ? omit(Array.from(skipped), filteredLockfile.packages) : {},
416}
417newHoistedDependencies = await hoist({
418extraNodePath: opts.extraNodePaths,
419lockfile: hoistLockfile,
420importerIds,
421preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
422privateHoistedModulesDir: hoistedModulesDir,
423privateHoistPattern: opts.hoistPattern ?? [],
424publicHoistedModulesDir,
425publicHoistPattern: opts.publicHoistPattern ?? [],
426virtualStoreDir,
427hoistedWorkspacePackages: opts.hoistWorkspacePackages
428? Object.values(opts.allProjects).reduce((hoistedWorkspacePackages, project) => {
429if (project.manifest.name && project.id !== '.') {
430hoistedWorkspacePackages[project.id] = {
431dir: project.rootDir,
432name: project.manifest.name,
433}
434}
435return hoistedWorkspacePackages
436}, {} as Record<string, HoistedWorkspaceProject>)
437: undefined,
438})
439} else {
440newHoistedDependencies = {}
441}
442
443await linkAllBins(graph, {
444extraNodePaths: opts.extraNodePaths,
445optional: opts.include.optionalDependencies,
446preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
447warn,
448})
449
450if ((currentLockfile != null) && !equals(importerIds.sort(), Object.keys(filteredLockfile.importers).sort())) {
451Object.assign(filteredLockfile.packages!, currentLockfile.packages)
452}
453
454/** Skip linking and due to no project manifest */
455if (!opts.ignorePackageManifest) {
456linkedToRoot = await symlinkDirectDependencies({
457dedupe: Boolean(opts.dedupeDirectDeps),
458directDependenciesByImporterId,
459filteredLockfile,
460lockfileDir,
461projects: selectedProjects,
462registries: opts.registries,
463symlink: opts.symlink,
464})
465}
466}
467
468if (opts.ignoreScripts) {
469for (const { id, manifest } of selectedProjects) {
470if (opts.ignoreScripts && ((manifest?.scripts) != null) &&
471(manifest.scripts.preinstall ?? manifest.scripts.prepublish ??
472manifest.scripts.install ??
473manifest.scripts.postinstall ??
474manifest.scripts.prepare)
475) {
476opts.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
480opts.pendingBuilds = opts.pendingBuilds
481.concat(
482depNodes
483.filter(({ requiresBuild }) => requiresBuild)
484.map(({ depPath }) => depPath)
485)
486}
487if (!opts.ignoreScripts || Object.keys(opts.patchedDependencies ?? {}).length > 0) {
488const directNodes = new Set<string>()
489for (const id of union(importerIds, ['.'])) {
490Object
491.values(directDependenciesByImporterId[id] ?? {})
492.filter((loc) => graph[loc])
493.forEach((loc) => {
494directNodes.add(loc)
495})
496}
497const extraBinPaths = [...opts.extraBinPaths ?? []]
498if (opts.hoistPattern != null) {
499extraBinPaths.unshift(path.join(virtualStoreDir, 'node_modules/.bin'))
500}
501let extraEnv: Record<string, string> | undefined = opts.extraEnv
502if (opts.enablePnp) {
503extraEnv = {
504...extraEnv,
505...makeNodeRequireOption(path.join(opts.lockfileDir, '.pnp.cjs')),
506}
507}
508await buildModules(graph, Array.from(directNodes), {
509allowBuild: createAllowBuildFunction(opts),
510childConcurrency: opts.childConcurrency,
511extraBinPaths,
512extraEnv,
513depsStateCache,
514ignoreScripts: opts.ignoreScripts || opts.ignoreDepScripts,
515hoistedLocations,
516lockfileDir,
517optional: opts.include.optionalDependencies,
518preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
519rawConfig: opts.rawConfig,
520rootModulesDir: virtualStoreDir,
521scriptsPrependNodePath: opts.scriptsPrependNodePath,
522scriptShell: opts.scriptShell,
523shellEmulator: opts.shellEmulator,
524sideEffectsCacheWrite: opts.sideEffectsCacheWrite,
525storeController: opts.storeController,
526unsafePerm: opts.unsafePerm,
527userAgent: opts.userAgent,
528})
529}
530
531const projectsToBeBuilt = extendProjectsWithTargetDirs(selectedProjects, wantedLockfile, {
532pkgLocationsByDepPath,
533virtualStoreDir,
534})
535
536if (opts.enableModulesDir !== false) {
537const rootProjectDeps = !opts.dedupeDirectDeps ? {} : (directDependenciesByImporterId['.'] ?? {})
538/** Skip linking and due to no project manifest */
539if (!opts.ignorePackageManifest) {
540await Promise.all(selectedProjects.map(async (project) => {
541if (opts.nodeLinker === 'hoisted' || opts.publicHoistPattern?.length && path.relative(opts.lockfileDir, project.rootDir) === '') {
542await linkBinsOfImporter(project, {
543extraNodePaths: opts.extraNodePaths,
544preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
545})
546} else {
547let directPkgDirs: string[]
548if (project.id === '.') {
549directPkgDirs = Object.values(directDependenciesByImporterId[project.id])
550} else {
551directPkgDirs = []
552for (const [alias, dir] of Object.entries(directDependenciesByImporterId[project.id])) {
553if (rootProjectDeps[alias] !== dir) {
554directPkgDirs.push(dir)
555}
556}
557}
558await linkBinsOfPackages(
559(
560await Promise.all(
561directPkgDirs.map(async (dir) => ({
562location: dir,
563manifest: await safeReadProjectManifestOnly(dir),
564}))
565)
566)
567.filter(({ manifest }) => manifest != null) as Array<{ location: string, manifest: DependencyManifest }>,
568project.binsDir,
569{
570extraNodePaths: opts.extraNodePaths,
571preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
572}
573)
574}
575}))
576}
577const injectedDeps: Record<string, string[]> = {}
578for (const project of projectsToBeBuilt) {
579if (project.targetDirs.length > 0) {
580injectedDeps[project.id] = project.targetDirs.map((targetDir) => path.relative(opts.lockfileDir, targetDir))
581}
582}
583await writeModulesManifest(rootModulesDir, {
584hoistedDependencies: newHoistedDependencies,
585hoistPattern: opts.hoistPattern,
586included: opts.include,
587injectedDeps,
588layoutVersion: LAYOUT_VERSION,
589hoistedLocations,
590nodeLinker: opts.nodeLinker,
591packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`,
592pendingBuilds: opts.pendingBuilds,
593publicHoistPattern: opts.publicHoistPattern,
594prunedAt: opts.pruneVirtualStore === true || opts.prunedAt == null
595? new Date().toUTCString()
596: opts.prunedAt,
597registries: opts.registries,
598skipped: Array.from(skipped),
599storeDir: opts.storeDir,
600virtualStoreDir,
601}, {
602makeModulesDir: Object.keys(filteredLockfile.packages ?? {}).length > 0,
603})
604if (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.
607await writeLockfiles({
608wantedLockfileDir: opts.lockfileDir,
609currentLockfileDir: virtualStoreDir,
610wantedLockfile,
611currentLockfile: filteredLockfile,
612})
613} else {
614await writeCurrentLockfile(virtualStoreDir, filteredLockfile)
615}
616}
617
618// waiting till package requests are finished
619await Promise.all(depNodes.map(async ({ fetching }) => {
620try {
621await fetching?.()
622} catch {}
623}))
624
625summaryLogger.debug({ prefix: lockfileDir })
626
627await opts.storeController.close()
628
629if (!opts.ignoreScripts && !opts.ignorePackageManifest) {
630await runLifecycleHooksConcurrently(
631['preinstall', 'install', 'postinstall', 'prepare'],
632projectsToBeBuilt,
633opts.childConcurrency ?? 5,
634scriptsOpts
635)
636}
637
638if ((reporter != null) && typeof reporter === 'function') {
639streamParser.removeListener('data', reporter)
640}
641return {
642stats: {
643added,
644removed,
645linkedToRoot,
646},
647}
648}
649
650type SymlinkDirectDependenciesOpts = Pick<HeadlessOptions, 'registries' | 'symlink' | 'lockfileDir'> & {
651filteredLockfile: Lockfile
652dedupe: boolean
653directDependenciesByImporterId: DirectDependenciesByImporterId
654projects: Project[]
655}
656
657async function symlinkDirectDependencies (
658{
659filteredLockfile,
660dedupe,
661directDependenciesByImporterId,
662lockfileDir,
663projects,
664registries,
665symlink,
666}: SymlinkDirectDependenciesOpts
667): Promise<number> {
668projects.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
671packageManifestLogger.debug({
672prefix: rootDir,
673updated: manifest,
674})
675})
676if (symlink === false) return 0
677const importerManifestsByImporterId = {} as { [id: string]: ProjectManifest }
678for (const { id, manifest } of projects) {
679importerManifestsByImporterId[id] = manifest
680}
681const projectsToLink = Object.fromEntries(await Promise.all(
682projects.map(async ({ rootDir, id, modulesDir }) => ([id, {
683dir: rootDir,
684modulesDir,
685dependencies: await getRootPackagesToLink(filteredLockfile, {
686importerId: id,
687importerModulesDir: modulesDir,
688lockfileDir,
689projectDir: rootDir,
690importerManifestsByImporterId,
691registries,
692rootDependencies: directDependenciesByImporterId[id],
693}),
694}]))
695))
696const rootProject = projectsToLink['.']
697if (rootProject && dedupe) {
698const rootDeps = Object.fromEntries(rootProject.dependencies.map((dep: LinkedDirectDep) => [dep.alias, dep.dir]))
699for (const project of Object.values(omit(['.'], projectsToLink))) {
700project.dependencies = project.dependencies.filter((dep: LinkedDirectDep) => dep.dir !== rootDeps[dep.alias])
701}
702}
703return linkDirectDeps(projectsToLink, { dedupe: Boolean(dedupe) })
704}
705
706async function linkBinsOfImporter (
707{ manifest, modulesDir, binsDir, rootDir }: {
708binsDir: string
709manifest: ProjectManifest
710modulesDir: string
711rootDir: string
712},
713{ extraNodePaths, preferSymlinkedExecutables }: { extraNodePaths?: string[], preferSymlinkedExecutables?: boolean } = {}
714): Promise<string[]> {
715const warn = (message: string) => {
716logger.info({ message, prefix: rootDir })
717}
718return linkBins(modulesDir, binsDir, {
719extraNodePaths,
720allowExoticManifests: true,
721preferSymlinkedExecutables,
722projectManifest: manifest,
723warn,
724})
725}
726
727async function getRootPackagesToLink (
728lockfile: Lockfile,
729opts: {
730registries: Registries
731projectDir: string
732importerId: string
733importerModulesDir: string
734importerManifestsByImporterId: { [id: string]: ProjectManifest }
735lockfileDir: string
736rootDependencies: { [alias: string]: string }
737}
738): Promise<LinkedDirectDep[]> {
739const projectSnapshot = lockfile.importers[opts.importerId]
740const allDeps = {
741...projectSnapshot.devDependencies,
742...projectSnapshot.dependencies,
743...projectSnapshot.optionalDependencies,
744}
745return (await Promise.all(
746Object.entries(allDeps)
747.map(async ([alias, ref]) => {
748if (ref.startsWith('link:')) {
749const isDev = Boolean(projectSnapshot.devDependencies?.[alias])
750const isOptional = Boolean(projectSnapshot.optionalDependencies?.[alias])
751const packageDir = path.join(opts.projectDir, ref.slice(5))
752const linkedPackage = await (async () => {
753const importerId = getLockfileImporterId(opts.lockfileDir, packageDir)
754if (opts.importerManifestsByImporterId[importerId]) {
755return opts.importerManifestsByImporterId[importerId]
756}
757try {
758// TODO: cover this case with a test
759return await readProjectManifestOnly(packageDir) as DependencyManifest
760} catch (err: any) { // eslint-disable-line
761if (err['code'] !== 'ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND') throw err
762return { name: alias, version: '0.0.0' }
763}
764})() as DependencyManifest
765return {
766alias,
767name: linkedPackage.name,
768version: linkedPackage.version,
769dir: packageDir,
770id: ref,
771isExternalLink: true,
772dependencyType: isDev && 'dev' ||
773isOptional && 'optional' ||
774'prod',
775}
776}
777const dir = opts.rootDependencies[alias]
778// Skipping linked packages
779if (!dir) {
780return
781}
782const isDev = Boolean(projectSnapshot.devDependencies?.[alias])
783const isOptional = Boolean(projectSnapshot.optionalDependencies?.[alias])
784
785const depPath = dp.refToRelative(ref, alias)
786if (depPath === null) return
787const pkgSnapshot = lockfile.packages?.[depPath]
788if (pkgSnapshot == null) return // this won't ever happen. Just making typescript happy
789const pkgId = pkgSnapshot.id ?? dp.refToRelative(ref, alias) ?? undefined
790const pkgInfo = nameVerFromPkgSnapshot(depPath, pkgSnapshot)
791return {
792alias,
793isExternalLink: false,
794name: pkgInfo.name,
795version: pkgInfo.version,
796dependencyType: isDev && 'dev' || isOptional && 'optional' || 'prod',
797dir,
798id: pkgId,
799}
800})
801))
802.filter(Boolean) as LinkedDirectDep[]
803}
804
805const limitLinking = pLimit(16)
806
807async function linkAllPkgs (
808storeController: StoreController,
809depNodes: DependenciesGraphNode[],
810opts: {
811depGraph: DependenciesGraph
812depsStateCache: DepsStateCache
813disableRelinkLocalDirDeps?: boolean
814force: boolean
815ignoreScripts: boolean
816lockfileDir: string
817sideEffectsCacheRead: boolean
818}
819): Promise<void> {
820await Promise.all(
821depNodes.map(async (depNode) => {
822if (!depNode.fetching) return
823let filesResponse!: PackageFilesResponse
824try {
825filesResponse = (await depNode.fetching()).files
826} catch (err: any) { // eslint-disable-line
827if (depNode.optional) return
828throw err
829}
830
831depNode.requiresBuild = filesResponse.requiresBuild
832let sideEffectsCacheKey: string | undefined
833if (opts.sideEffectsCacheRead && filesResponse.sideEffects && !isEmpty(filesResponse.sideEffects)) {
834sideEffectsCacheKey = calcDepState(opts.depGraph, opts.depsStateCache, depNode.dir, {
835isBuilt: !opts.ignoreScripts && depNode.requiresBuild,
836patchFileHash: depNode.patchFile?.hash,
837})
838}
839const { importMethod, isBuilt } = await storeController.importPackage(depNode.dir, {
840filesResponse,
841force: opts.force,
842disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps,
843requiresBuild: depNode.patchFile != null || depNode.requiresBuild,
844sideEffectsCacheKey,
845})
846if (importMethod) {
847progressLogger.debug({
848method: importMethod,
849requester: opts.lockfileDir,
850status: 'imported',
851to: depNode.dir,
852})
853}
854depNode.isBuilt = isBuilt
855
856const selfDep = depNode.children[depNode.name]
857if (selfDep) {
858const pkg = opts.depGraph[selfDep]
859if (!pkg) return
860const targetModulesDir = path.join(depNode.modules, depNode.name, 'node_modules')
861await limitLinking(async () => symlinkDependency(pkg.dir, targetModulesDir, depNode.name))
862}
863})
864)
865}
866
867async function linkAllBins (
868depGraph: DependenciesGraph,
869opts: {
870extraNodePaths?: string[]
871optional: boolean
872preferSymlinkedExecutables?: boolean
873warn: (message: string) => void
874}
875): Promise<void> {
876await Promise.all(
877Object.values(depGraph)
878.map(async (depNode) => limitLinking(async () => {
879const childrenToLink: Record<string, string> = opts.optional
880? depNode.children
881: pickBy((_, childAlias) => !depNode.optionalDependencies.has(childAlias), depNode.children)
882
883const binPath = path.join(depNode.dir, 'node_modules/.bin')
884const pkgSnapshots = props<string, DependenciesGraphNode>(Object.values(childrenToLink), depGraph)
885
886if (pkgSnapshots.includes(undefined as any)) { // eslint-disable-line
887await linkBins(depNode.modules, binPath, {
888extraNodePaths: opts.extraNodePaths,
889preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
890warn: opts.warn,
891})
892} else {
893const pkgs = await Promise.all(
894pkgSnapshots
895.filter(({ hasBin }) => hasBin)
896.map(async ({ dir }) => ({
897location: dir,
898manifest: await readPackageJsonFromDir(dir) as DependencyManifest,
899}))
900)
901
902await linkBinsOfPackages(pkgs, binPath, {
903extraNodePaths: opts.extraNodePaths,
904preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
905})
906}
907
908// link also the bundled dependencies` bins
909if (depNode.hasBundledDependencies) {
910const bundledModules = path.join(depNode.dir, 'node_modules')
911await linkBins(bundledModules, binPath, {
912extraNodePaths: opts.extraNodePaths,
913preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
914warn: opts.warn,
915})
916}
917}))
918)
919}
920
921async function linkAllModules (
922depNodes: Array<Pick<DependenciesGraphNode, 'children' | 'optionalDependencies' | 'modules' | 'name'>>,
923opts: {
924optional: boolean
925}
926): Promise<void> {
927await symlinkAllModules({
928deps: depNodes.map((depNode) => ({
929children: opts.optional
930? depNode.children
931: pickBy((_, childAlias) => !depNode.optionalDependencies.has(childAlias), depNode.children),
932modules: depNode.modules,
933name: depNode.name,
934})),
935})
936}
937