@pnpm/installing.linking.hoist 1002.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2016 Rico Sta. Cruz and other contributors
4
+ Copyright (c) 2016-2026 Zoltan Kochan and other contributors
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @pnpm/hoist
2
+
3
+ > Hoists dependencies in a node_modules created by pnpm
4
+
5
+ Formerly `@pnpm/shamefully-flatten`.
6
+
7
+ ## Installation
8
+
9
+ ```
10
+ pnpm add @pnpm/hoist
11
+ ```
12
+
13
+ ## License
14
+
15
+ MIT
package/lib/index.d.ts ADDED
@@ -0,0 +1,71 @@
1
+ import type { DependenciesField, DepPath, HoistedDependencies, ProjectId } from '@pnpm/types';
2
+ export interface DependenciesGraphNode<T extends string> {
3
+ dir: string;
4
+ children: Record<string, T>;
5
+ optionalDependencies: Set<string>;
6
+ hasBin: boolean;
7
+ name: string;
8
+ depPath: DepPath;
9
+ }
10
+ export type DependenciesGraph<T extends string> = Record<T, DependenciesGraphNode<T>>;
11
+ export interface DirectDependenciesByImporterId<T extends string> {
12
+ [importerId: string]: Map<string, T>;
13
+ }
14
+ export interface HoistOpts<T extends string> extends GetHoistedDependenciesOpts<T> {
15
+ extraNodePath?: string[];
16
+ preferSymlinkedExecutables?: boolean;
17
+ virtualStoreDir: string;
18
+ virtualStoreDirMaxLength: number;
19
+ }
20
+ export declare function hoist<T extends string>(opts: HoistOpts<T>): Promise<HoistedDependencies | null>;
21
+ export interface GetHoistedDependenciesOpts<T extends string> {
22
+ graph: DependenciesGraph<T>;
23
+ skipped: Set<DepPath>;
24
+ directDepsByImporterId: DirectDependenciesByImporterId<T>;
25
+ importerIds?: ProjectId[];
26
+ privateHoistPattern: string[];
27
+ privateHoistedModulesDir: string;
28
+ publicHoistPattern: string[];
29
+ publicHoistedModulesDir: string;
30
+ hoistedWorkspacePackages?: Record<ProjectId, HoistedWorkspaceProject>;
31
+ }
32
+ export interface HoistedWorkspaceProject {
33
+ name: string;
34
+ dir: string;
35
+ }
36
+ export declare function getHoistedDependencies<T extends string>(opts: GetHoistedDependenciesOpts<T>): HoistGraphResult<T> | null;
37
+ export interface Dependency<T extends string> {
38
+ children: Record<string, T | ProjectId>;
39
+ nodeId: T;
40
+ depth: number;
41
+ }
42
+ interface HoistGraphResult<T extends string> {
43
+ hoistedDependencies: HoistedDependencies;
44
+ hoistedDependenciesByNodeId: HoistedDependenciesByNodeId<T>;
45
+ hoistedAliasesWithBins: string[];
46
+ }
47
+ type HoistedDependenciesByNodeId<T extends string> = Map<T | ProjectId, Record<string, 'public' | 'private'>>;
48
+ export declare function graphWalker<T extends string>(graph: DependenciesGraph<T>, directDepsByImporterId: DirectDependenciesByImporterId<T>, opts?: {
49
+ include?: {
50
+ [dependenciesField in DependenciesField]: boolean;
51
+ };
52
+ skipped?: Set<DepPath>;
53
+ }): GraphWalker<T>;
54
+ export interface GraphWalker<T extends string> {
55
+ directDeps: Array<{
56
+ alias: string;
57
+ nodeId: T;
58
+ }>;
59
+ step: GraphWalkerStep<T>;
60
+ }
61
+ export interface GraphWalkerStep<T extends string> {
62
+ dependencies: Array<GraphDependency<T>>;
63
+ links: string[];
64
+ missing: string[];
65
+ }
66
+ export interface GraphDependency<T extends string> {
67
+ nodeId: T;
68
+ node: DependenciesGraphNode<T>;
69
+ next: () => GraphWalkerStep<T>;
70
+ }
71
+ export {};
package/lib/index.js ADDED
@@ -0,0 +1,305 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { linkBinsOfPkgsByAliases } from '@pnpm/bins.linker';
4
+ import { createMatcher } from '@pnpm/config.matcher';
5
+ import { WANTED_LOCKFILE } from '@pnpm/constants';
6
+ import { linkLogger } from '@pnpm/core-loggers';
7
+ import { logger } from '@pnpm/logger';
8
+ import { lexCompare } from '@pnpm/util.lex-comparator';
9
+ import { isSubdir } from 'is-subdir';
10
+ import { resolveLinkTarget } from 'resolve-link-target';
11
+ import symlinkDir from 'symlink-dir';
12
+ const hoistLogger = logger('hoist');
13
+ export async function hoist(opts) {
14
+ const result = getHoistedDependencies(opts);
15
+ if (!result)
16
+ return null;
17
+ const { hoistedDependencies, hoistedAliasesWithBins, hoistedDependenciesByNodeId } = result;
18
+ await symlinkHoistedDependencies(hoistedDependenciesByNodeId, {
19
+ graph: opts.graph,
20
+ directDepsByImporterId: opts.directDepsByImporterId,
21
+ privateHoistedModulesDir: opts.privateHoistedModulesDir,
22
+ publicHoistedModulesDir: opts.publicHoistedModulesDir,
23
+ virtualStoreDir: opts.virtualStoreDir,
24
+ virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
25
+ hoistedWorkspacePackages: opts.hoistedWorkspacePackages,
26
+ });
27
+ // Here we only link the bins of the privately hoisted modules.
28
+ // The bins of the publicly hoisted modules will be linked together with
29
+ // the bins of the project's direct dependencies.
30
+ // This is possible because the publicly hoisted modules
31
+ // are in the same directory as the regular dependencies.
32
+ await linkAllBins(opts.privateHoistedModulesDir, {
33
+ extraNodePaths: opts.extraNodePath,
34
+ hoistedAliasesWithBins,
35
+ preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
36
+ });
37
+ return hoistedDependencies;
38
+ }
39
+ export function getHoistedDependencies(opts) {
40
+ if (Object.keys(opts.graph ?? {}).length === 0)
41
+ return null;
42
+ const { directDeps, step } = graphWalker(opts.graph, opts.directDepsByImporterId);
43
+ // We want to hoist all the workspace packages, not only those that are in the dependencies
44
+ // of any other workspace packages.
45
+ // That is why we can't just simply use the lockfile walker to include links to local workspace packages too.
46
+ // We have to explicitly include all the workspace packages.
47
+ const hoistedWorkspaceDeps = Object.fromEntries(Object.entries(opts.hoistedWorkspacePackages ?? {})
48
+ .map(([id, { name }]) => [name, id]));
49
+ const deps = [
50
+ {
51
+ children: {
52
+ ...hoistedWorkspaceDeps,
53
+ ...directDeps
54
+ .reduce((acc, { alias, nodeId }) => {
55
+ if (!acc[alias]) {
56
+ acc[alias] = nodeId;
57
+ }
58
+ return acc;
59
+ }, {}),
60
+ },
61
+ nodeId: '',
62
+ depth: -1,
63
+ },
64
+ ...getDependencies(0, step),
65
+ ];
66
+ const getAliasHoistType = createGetAliasHoistType(opts.publicHoistPattern, opts.privateHoistPattern);
67
+ return hoistGraph(deps, opts.directDepsByImporterId['.'] ?? new Map(), {
68
+ getAliasHoistType,
69
+ graph: opts.graph,
70
+ skipped: opts.skipped,
71
+ });
72
+ }
73
+ function createGetAliasHoistType(publicHoistPattern, privateHoistPattern) {
74
+ const publicMatcher = createMatcher(publicHoistPattern);
75
+ const privateMatcher = createMatcher(privateHoistPattern);
76
+ return (alias) => {
77
+ if (publicMatcher(alias))
78
+ return 'public';
79
+ if (privateMatcher(alias))
80
+ return 'private';
81
+ return false;
82
+ };
83
+ }
84
+ async function linkAllBins(modulesDir, opts) {
85
+ const bin = path.join(modulesDir, '.bin');
86
+ const warn = (message, code) => {
87
+ if (code === 'BINARIES_CONFLICT')
88
+ return;
89
+ logger.info({ message, prefix: path.join(modulesDir, '../..') });
90
+ };
91
+ try {
92
+ await linkBinsOfPkgsByAliases(opts.hoistedAliasesWithBins, bin, {
93
+ allowExoticManifests: true,
94
+ extraNodePaths: opts.extraNodePaths,
95
+ modulesDir,
96
+ preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
97
+ warn,
98
+ });
99
+ }
100
+ catch (err) { // eslint-disable-line
101
+ // Some packages generate their commands with lifecycle hooks.
102
+ // At this stage, such commands are not generated yet.
103
+ // For now, we don't hoist such generated commands.
104
+ // Related issue: https://github.com/pnpm/pnpm/issues/2071
105
+ }
106
+ }
107
+ function getDependencies(depth, step) {
108
+ const deps = [];
109
+ const nextSteps = [];
110
+ for (const { node, nodeId, next } of step.dependencies) {
111
+ deps.push({
112
+ children: node.children,
113
+ nodeId,
114
+ depth,
115
+ });
116
+ nextSteps.push(next());
117
+ }
118
+ for (const depPath of step.missing) {
119
+ // It might make sense to fail if the depPath is not in the skipped list from .modules.yaml
120
+ // However, the skipped list currently contains package IDs, not dep paths.
121
+ logger.debug({ message: `No entry for "${depPath}" in ${WANTED_LOCKFILE}` });
122
+ }
123
+ return [
124
+ ...deps,
125
+ ...nextSteps.flatMap(getDependencies.bind(null, depth + 1)),
126
+ ];
127
+ }
128
+ function hoistGraph(depNodes, currentSpecifiers, opts) {
129
+ const hoistedAliases = new Set(currentSpecifiers.keys());
130
+ const hoistedDependencies = Object.create(null);
131
+ const hoistedDependenciesByNodeId = new Map();
132
+ const hoistedAliasesWithBins = new Set();
133
+ depNodes
134
+ // sort by depth and then alphabetically
135
+ .sort((a, b) => {
136
+ const depthDiff = a.depth - b.depth;
137
+ return depthDiff === 0 ? lexCompare(a.nodeId, b.nodeId) : depthDiff;
138
+ })
139
+ // build the alias map and the id map
140
+ .forEach((depNode) => {
141
+ for (const [childAlias, childNodeId] of Object.entries(depNode.children)) {
142
+ const hoist = opts.getAliasHoistType(childAlias);
143
+ if (!hoist)
144
+ continue;
145
+ const childAliasNormalized = childAlias.toLowerCase();
146
+ // if this alias has already been taken, skip it
147
+ if (hoistedAliases.has(childAliasNormalized)) {
148
+ continue;
149
+ }
150
+ if (!hoistedDependenciesByNodeId.has(childNodeId)) {
151
+ hoistedDependenciesByNodeId.set(childNodeId, {});
152
+ }
153
+ hoistedDependenciesByNodeId.get(childNodeId)[childAlias] = hoist;
154
+ const node = opts.graph[childNodeId];
155
+ if (node?.depPath == null || opts.skipped.has(node.depPath)) {
156
+ continue;
157
+ }
158
+ if (node.hasBin) {
159
+ hoistedAliasesWithBins.add(childAlias);
160
+ }
161
+ hoistedAliases.add(childAliasNormalized);
162
+ if (!hoistedDependencies[node.depPath]) {
163
+ hoistedDependencies[node.depPath] = {};
164
+ }
165
+ hoistedDependencies[node.depPath][childAlias] = hoist;
166
+ }
167
+ });
168
+ return {
169
+ hoistedDependencies,
170
+ hoistedDependenciesByNodeId,
171
+ hoistedAliasesWithBins: Array.from(hoistedAliasesWithBins),
172
+ };
173
+ }
174
+ async function symlinkHoistedDependencies(hoistedDependenciesByNodeId, opts) {
175
+ const symlink = symlinkHoistedDependency.bind(null, opts);
176
+ const promises = [];
177
+ for (const [hoistedDepNodeId, pkgAliases] of hoistedDependenciesByNodeId.entries()) {
178
+ promises.push((async () => {
179
+ const node = opts.graph[hoistedDepNodeId];
180
+ let depLocation;
181
+ if (node) {
182
+ depLocation = node.dir;
183
+ }
184
+ else {
185
+ if (!opts.directDepsByImporterId[hoistedDepNodeId]) {
186
+ // This dependency is probably a skipped optional dependency.
187
+ hoistLogger.debug({ hoistFailedFor: hoistedDepNodeId });
188
+ return;
189
+ }
190
+ depLocation = opts.hoistedWorkspacePackages[hoistedDepNodeId].dir;
191
+ }
192
+ await Promise.all(Object.entries(pkgAliases).map(async ([pkgAlias, hoistType]) => {
193
+ const targetDir = hoistType === 'public'
194
+ ? opts.publicHoistedModulesDir
195
+ : opts.privateHoistedModulesDir;
196
+ const dest = path.join(targetDir, pkgAlias);
197
+ return symlink(depLocation, dest);
198
+ }));
199
+ })());
200
+ }
201
+ await Promise.all(promises);
202
+ }
203
+ async function symlinkHoistedDependency(opts, depLocation, dest) {
204
+ try {
205
+ await symlinkDir(depLocation, dest, { overwrite: false });
206
+ linkLogger.debug({ target: dest, link: depLocation });
207
+ return;
208
+ }
209
+ catch (err) { // eslint-disable-line
210
+ if (err.code !== 'EEXIST' && err.code !== 'EISDIR')
211
+ throw err;
212
+ }
213
+ let existingSymlink;
214
+ try {
215
+ existingSymlink = await resolveLinkTarget(dest);
216
+ }
217
+ catch {
218
+ hoistLogger.debug({
219
+ skipped: dest,
220
+ reason: 'a directory is present at the target location',
221
+ });
222
+ return;
223
+ }
224
+ if (!isSubdir(opts.virtualStoreDir, existingSymlink)) {
225
+ hoistLogger.debug({
226
+ skipped: dest,
227
+ existingSymlink,
228
+ reason: 'an external symlink is present at the target location',
229
+ });
230
+ return;
231
+ }
232
+ await fs.promises.unlink(dest);
233
+ await symlinkDir(depLocation, dest);
234
+ linkLogger.debug({ target: dest, link: depLocation });
235
+ }
236
+ export function graphWalker(graph, directDepsByImporterId, opts) {
237
+ const startNodeIds = [];
238
+ const allDirectDeps = [];
239
+ for (const directDeps of Object.values(directDepsByImporterId)) {
240
+ for (const [alias, nodeId] of directDeps.entries()) {
241
+ const depNode = graph[nodeId];
242
+ if (depNode == null)
243
+ continue;
244
+ startNodeIds.push(nodeId);
245
+ allDirectDeps.push({ alias, nodeId });
246
+ }
247
+ }
248
+ const visited = new Set();
249
+ return {
250
+ directDeps: allDirectDeps,
251
+ step: makeStep({
252
+ includeOptionalDependencies: opts?.include?.optionalDependencies !== false,
253
+ graph,
254
+ visited,
255
+ skipped: opts?.skipped,
256
+ }, startNodeIds),
257
+ };
258
+ }
259
+ function makeStep(ctx, nextNodeIds) {
260
+ const result = {
261
+ dependencies: [],
262
+ links: [],
263
+ missing: [],
264
+ };
265
+ const _next = collectChildNodeIds.bind(null, {
266
+ includeOptionalDependencies: ctx.includeOptionalDependencies,
267
+ });
268
+ for (const nodeId of nextNodeIds) {
269
+ if (ctx.visited.has(nodeId))
270
+ continue;
271
+ ctx.visited.add(nodeId);
272
+ const node = ctx.graph[nodeId];
273
+ if (node == null) {
274
+ if (nodeId.startsWith('link:')) {
275
+ result.links.push(nodeId);
276
+ continue;
277
+ }
278
+ result.missing.push(nodeId);
279
+ continue;
280
+ }
281
+ if (ctx.skipped?.has(node.depPath))
282
+ continue;
283
+ result.dependencies.push({
284
+ nodeId,
285
+ next: () => makeStep(ctx, _next(node)),
286
+ node,
287
+ });
288
+ }
289
+ return result;
290
+ }
291
+ function collectChildNodeIds(opts, nextPkg) {
292
+ if (opts.includeOptionalDependencies) {
293
+ return Object.values(nextPkg.children);
294
+ }
295
+ else {
296
+ const nextNodeIds = [];
297
+ for (const [alias, nodeId] of Object.entries(nextPkg.children)) {
298
+ if (!nextPkg.optionalDependencies.has(alias)) {
299
+ nextNodeIds.push(nodeId);
300
+ }
301
+ }
302
+ return nextNodeIds;
303
+ }
304
+ }
305
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@pnpm/installing.linking.hoist",
3
+ "version": "1002.0.8",
4
+ "description": "Hoists dependencies in a node_modules created by pnpm",
5
+ "keywords": [
6
+ "pnpm",
7
+ "pnpm11"
8
+ ],
9
+ "license": "MIT",
10
+ "funding": "https://opencollective.com/pnpm",
11
+ "repository": "https://github.com/pnpm/pnpm/tree/main/installing/linking/hoist",
12
+ "homepage": "https://github.com/pnpm/pnpm/tree/main/installing/linking/hoist#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/pnpm/pnpm/issues"
15
+ },
16
+ "type": "module",
17
+ "main": "lib/index.js",
18
+ "types": "lib/index.d.ts",
19
+ "exports": {
20
+ ".": "./lib/index.js"
21
+ },
22
+ "files": [
23
+ "lib",
24
+ "!*.map"
25
+ ],
26
+ "directories": {
27
+ "test": "test"
28
+ },
29
+ "dependencies": {
30
+ "@pnpm/util.lex-comparator": "^3.0.2",
31
+ "is-subdir": "^2.0.0",
32
+ "ramda": "npm:@pnpm/ramda@0.28.1",
33
+ "resolve-link-target": "^3.0.0",
34
+ "symlink-dir": "^7.0.0",
35
+ "@pnpm/bins.linker": "1000.2.6",
36
+ "@pnpm/core-loggers": "1001.0.4",
37
+ "@pnpm/constants": "1001.3.1",
38
+ "@pnpm/types": "1000.9.0",
39
+ "@pnpm/config.matcher": "1000.1.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@pnpm/logger": ">=1001.0.0 <1002.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/ramda": "0.29.12",
46
+ "@pnpm/logger": "1001.0.1",
47
+ "@pnpm/installing.linking.hoist": "1002.0.8"
48
+ },
49
+ "engines": {
50
+ "node": ">=22.13"
51
+ },
52
+ "jest": {
53
+ "preset": "@pnpm/jest-config"
54
+ },
55
+ "scripts": {
56
+ "start": "tsgo --watch",
57
+ "test": "pnpm run compile",
58
+ "lint": "eslint \"src/**/*.ts\"",
59
+ "compile": "tsgo --build && pnpm run lint --fix"
60
+ }
61
+ }