@pnpm/store.controller 1004.0.0

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,13 @@
1
+ # @pnpm/package-store
2
+
3
+ > A storage for packages
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ pnpm add @pnpm/package-store
9
+ ```
10
+
11
+ ## License
12
+
13
+ [MIT](LICENSE)
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { type CafsLocker, createPackageStore, type CreatePackageStoreOptions } from './storeController/index.js';
2
+ export { getRegisteredProjects, registerProject } from './storeController/projectRegistry.js';
3
+ export * from '@pnpm/store.controller-types';
package/lib/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createPackageStore } from './storeController/index.js';
2
+ export { getRegisteredProjects, registerProject } from './storeController/projectRegistry.js';
3
+ export * from '@pnpm/store.controller-types';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,27 @@
1
+ import type { Fetchers } from '@pnpm/fetching.fetcher-base';
2
+ import type { CustomFetcher } from '@pnpm/hooks.types';
3
+ import type { ResolveFunction } from '@pnpm/resolving.resolver-base';
4
+ import type { ImportIndexedPackageAsync, StoreController } from '@pnpm/store.controller-types';
5
+ import { type CafsLocker } from '@pnpm/store.create-cafs-store';
6
+ import type { StoreIndex } from '@pnpm/store.index';
7
+ export { type CafsLocker };
8
+ export interface CreatePackageStoreOptions {
9
+ cafsLocker?: CafsLocker;
10
+ engineStrict?: boolean;
11
+ force?: boolean;
12
+ nodeVersion?: string;
13
+ importPackage?: ImportIndexedPackageAsync;
14
+ pnpmVersion?: string;
15
+ ignoreFile?: (filename: string) => boolean;
16
+ cacheDir: string;
17
+ storeDir: string;
18
+ networkConcurrency?: number;
19
+ packageImportMethod?: 'auto' | 'hardlink' | 'copy' | 'clone' | 'clone-or-copy';
20
+ verifyStoreIntegrity: boolean;
21
+ virtualStoreDirMaxLength: number;
22
+ strictStorePkgContentCheck?: boolean;
23
+ clearResolutionCache: () => void;
24
+ customFetchers?: CustomFetcher[];
25
+ storeIndex: StoreIndex;
26
+ }
27
+ export declare function createPackageStore(resolve: ResolveFunction, fetchers: Fetchers, initOpts: CreatePackageStoreOptions): StoreController;
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createPackageRequester } from '@pnpm/installing.package-requester';
4
+ import { createCafsStore, createPackageImporterAsync } from '@pnpm/store.create-cafs-store';
5
+ import { addFilesFromDir, importPackage, initStoreDir } from '@pnpm/worker';
6
+ import { prune } from './prune.js';
7
+ export {};
8
+ export function createPackageStore(resolve, fetchers, initOpts) {
9
+ const storeDir = initOpts.storeDir;
10
+ if (!fs.existsSync(path.join(storeDir, 'files'))) {
11
+ initStoreDir(storeDir).catch();
12
+ }
13
+ const cafs = createCafsStore(storeDir, {
14
+ cafsLocker: initOpts.cafsLocker,
15
+ packageImportMethod: initOpts.packageImportMethod,
16
+ });
17
+ const packageRequester = createPackageRequester({
18
+ force: initOpts.force,
19
+ engineStrict: initOpts.engineStrict,
20
+ nodeVersion: initOpts.nodeVersion,
21
+ pnpmVersion: initOpts.pnpmVersion,
22
+ resolve,
23
+ fetchers,
24
+ cafs,
25
+ ignoreFile: initOpts.ignoreFile,
26
+ networkConcurrency: initOpts.networkConcurrency,
27
+ storeDir: initOpts.storeDir,
28
+ verifyStoreIntegrity: initOpts.verifyStoreIntegrity,
29
+ virtualStoreDirMaxLength: initOpts.virtualStoreDirMaxLength,
30
+ strictStorePkgContentCheck: initOpts.strictStorePkgContentCheck,
31
+ customFetchers: initOpts.customFetchers,
32
+ });
33
+ return {
34
+ close: async () => {
35
+ initOpts.storeIndex.flush();
36
+ },
37
+ fetchPackage: packageRequester.fetchPackageToStore,
38
+ getFilesIndexFilePath: packageRequester.getFilesIndexFilePath,
39
+ importPackage: initOpts.importPackage
40
+ ? createPackageImporterAsync({ importIndexedPackage: initOpts.importPackage, storeDir: cafs.storeDir })
41
+ : (targetDir, opts) => importPackage({
42
+ ...opts,
43
+ packageImportMethod: initOpts.packageImportMethod,
44
+ storeDir: initOpts.storeDir,
45
+ targetDir,
46
+ }),
47
+ prune: prune.bind(null, { storeDir, cacheDir: initOpts.cacheDir, storeIndex: initOpts.storeIndex }),
48
+ requestPackage: packageRequester.requestPackage,
49
+ upload,
50
+ clearResolutionCache: initOpts.clearResolutionCache,
51
+ };
52
+ async function upload(builtPkgLocation, opts) {
53
+ await addFilesFromDir({
54
+ storeDir: cafs.storeDir,
55
+ storeIndex: initOpts.storeIndex,
56
+ dir: builtPkgLocation,
57
+ sideEffectsCacheKey: opts.sideEffectsCacheKey,
58
+ filesIndexFile: opts.filesIndexFile,
59
+ pkg: {},
60
+ });
61
+ }
62
+ }
63
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ export declare function getProjectsRegistryDir(storeDir: string): string;
2
+ /**
3
+ * Register a project as using the store.
4
+ * Creates a symlink in {storeDir}/projects/{hash} → {projectDir}
5
+ */
6
+ export declare function registerProject(storeDir: string, projectDir: string): Promise<void>;
7
+ /**
8
+ * Get all registered projects that use the global virtual store.
9
+ * Cleans up stale entries (projects that no longer exist).
10
+ */
11
+ export declare function getRegisteredProjects(storeDir: string): Promise<string[]>;
@@ -0,0 +1,90 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import util from 'node:util';
4
+ import { createShortHash } from '@pnpm/crypto.hash';
5
+ import { PnpmError } from '@pnpm/error';
6
+ import { globalInfo } from '@pnpm/logger';
7
+ import { isSubdir } from 'is-subdir';
8
+ import symlinkDir from 'symlink-dir';
9
+ const PROJECTS_DIR = 'projects';
10
+ export function getProjectsRegistryDir(storeDir) {
11
+ return path.join(storeDir, PROJECTS_DIR);
12
+ }
13
+ /**
14
+ * Register a project as using the store.
15
+ * Creates a symlink in {storeDir}/projects/{hash} → {projectDir}
16
+ */
17
+ export async function registerProject(storeDir, projectDir) {
18
+ // Avoid creating circular symlinks when the store is inside the project directory
19
+ if (isSubdir(projectDir, storeDir)) {
20
+ return;
21
+ }
22
+ const registryDir = getProjectsRegistryDir(storeDir);
23
+ await fs.mkdir(registryDir, { recursive: true });
24
+ const linkPath = path.join(registryDir, createShortHash(projectDir));
25
+ // symlink-dir handles the case where the symlink already exists
26
+ await symlinkDir(projectDir, linkPath);
27
+ }
28
+ /**
29
+ * Get all registered projects that use the global virtual store.
30
+ * Cleans up stale entries (projects that no longer exist).
31
+ */
32
+ export async function getRegisteredProjects(storeDir) {
33
+ const registryDir = getProjectsRegistryDir(storeDir);
34
+ let entries;
35
+ try {
36
+ entries = await fs.readdir(registryDir, { withFileTypes: true });
37
+ }
38
+ catch (err) {
39
+ if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
40
+ return [];
41
+ }
42
+ throw err;
43
+ }
44
+ const projects = [];
45
+ await Promise.all(entries.map(async (entry) => {
46
+ if (entry.name.startsWith('.'))
47
+ return;
48
+ // We expect only symlinks (or junctions on Windows) in the registry
49
+ if (!entry.isSymbolicLink())
50
+ return;
51
+ const linkPath = path.join(registryDir, entry.name);
52
+ // Read the symlink target - if this fails, it's an invalid entry
53
+ let target;
54
+ try {
55
+ target = await fs.readlink(linkPath);
56
+ }
57
+ catch (err) {
58
+ // If the file is not a symlink (EINVAL) or doesn't exist (ENOENT), ignore it
59
+ if (util.types.isNativeError(err) && 'code' in err && (err.code === 'ENOENT' || err.code === 'EINVAL')) {
60
+ return;
61
+ }
62
+ // For permission errors etc, inform the user
63
+ const message = util.types.isNativeError(err) ? err.message : String(err);
64
+ throw new PnpmError('PROJECT_REGISTRY_ENTRY_INACCESSIBLE', `Cannot read project registry entry "${linkPath}": ${message}`, {
65
+ hint: `To remove this project from the registry, delete the file at:\n ${linkPath}`,
66
+ });
67
+ }
68
+ const absoluteTarget = path.isAbsolute(target) ? target : path.resolve(path.dirname(linkPath), target);
69
+ // Check if project still exists
70
+ try {
71
+ await fs.stat(absoluteTarget);
72
+ projects.push(absoluteTarget);
73
+ }
74
+ catch (err) {
75
+ // Only clean up if project directory no longer exists
76
+ if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
77
+ await fs.unlink(linkPath);
78
+ globalInfo(`Removed stale project registry entry: ${absoluteTarget}`);
79
+ return;
80
+ }
81
+ // Can't access project - throw error to prevent incorrect pruning
82
+ const message = util.types.isNativeError(err) ? err.message : String(err);
83
+ throw new PnpmError('PROJECT_INACCESSIBLE', `Cannot access registered project "${absoluteTarget}": ${message}`, {
84
+ hint: `To remove this project from the registry, delete the symlink at:\n ${linkPath}`,
85
+ });
86
+ }
87
+ }));
88
+ return projects;
89
+ }
90
+ //# sourceMappingURL=projectRegistry.js.map
@@ -0,0 +1,7 @@
1
+ import type { StoreIndex } from '@pnpm/store.index';
2
+ export interface PruneOptions {
3
+ cacheDir: string;
4
+ storeDir: string;
5
+ storeIndex: StoreIndex;
6
+ }
7
+ export declare function prune({ cacheDir, storeDir, storeIndex }: PruneOptions, removeAlienFiles?: boolean): Promise<void>;
@@ -0,0 +1,91 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import util from 'node:util';
4
+ import { globalInfo, globalWarn } from '@pnpm/logger';
5
+ import { rimraf } from '@zkochan/rimraf';
6
+ import { pruneGlobalVirtualStore } from './pruneGlobalVirtualStore.js';
7
+ const BIG_ONE = BigInt(1);
8
+ export async function prune({ cacheDir, storeDir, storeIndex }, removeAlienFiles) {
9
+ // 1. First, prune the global virtual store
10
+ // This must happen BEFORE pruning the CAS, because removing packages from
11
+ // the virtual store will reduce hard link counts on files in the CAS
12
+ await pruneGlobalVirtualStore(storeDir);
13
+ // 2. Clean up metadata cache
14
+ const metadataDirs = await getSubdirsSafely(cacheDir);
15
+ await Promise.all(metadataDirs.map(async (metadataDir) => {
16
+ if (!metadataDir.startsWith('metadata'))
17
+ return;
18
+ try {
19
+ await rimraf(path.join(cacheDir, metadataDir));
20
+ }
21
+ catch (err) {
22
+ if (!(util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT')) {
23
+ throw err;
24
+ }
25
+ }
26
+ }));
27
+ await rimraf(path.join(storeDir, 'tmp'));
28
+ globalInfo('Removed all cached metadata files');
29
+ // 3. Prune the content-addressable store (CAS)
30
+ const cafsDir = path.join(storeDir, 'files');
31
+ const removedHashes = new Set();
32
+ const dirs = await getSubdirsSafely(cafsDir);
33
+ let fileCounter = 0;
34
+ await Promise.all(dirs.map(async (dir) => {
35
+ const subdir = path.join(cafsDir, dir);
36
+ await Promise.all((await fs.readdir(subdir)).map(async (fileName) => {
37
+ const filePath = path.join(subdir, fileName);
38
+ const stat = await fs.stat(filePath);
39
+ if (stat.isDirectory()) {
40
+ if (removeAlienFiles) {
41
+ await rimraf(filePath);
42
+ globalWarn(`An alien directory has been removed from the store: ${filePath}`);
43
+ fileCounter++;
44
+ return;
45
+ }
46
+ else {
47
+ globalWarn(`An alien directory is present in the store: ${filePath}`);
48
+ return;
49
+ }
50
+ }
51
+ if (stat.nlink === 1 || stat.nlink === BIG_ONE) {
52
+ await fs.unlink(filePath);
53
+ fileCounter++;
54
+ // Store the hex digest, which matches the format stored in PackageFileInfo.digest
55
+ // The file name in the store is the hex representation of the hash (with optional -exec suffix)
56
+ removedHashes.add(`${dir}${fileName.replace(/-exec$/, '')}`);
57
+ }
58
+ }));
59
+ }));
60
+ globalInfo(`Removed ${fileCounter} file${fileCounter === 1 ? '' : 's'}`);
61
+ // 4. Clean up orphaned package index entries
62
+ let pkgCounter = 0;
63
+ const toDelete = [];
64
+ for (const [filesIndexFile, data] of storeIndex.entries()) {
65
+ const pkgFilesIndex = data;
66
+ const pkgJson = pkgFilesIndex.files.get('package.json');
67
+ // TODO: implement prune of Node.js packages, they don't have a package.json file
68
+ if (pkgJson && removedHashes.has(pkgJson.digest)) {
69
+ toDelete.push(filesIndexFile);
70
+ pkgCounter++;
71
+ }
72
+ }
73
+ storeIndex.deleteMany(toDelete);
74
+ globalInfo(`Removed ${pkgCounter} package${pkgCounter === 1 ? '' : 's'}`);
75
+ }
76
+ async function getSubdirsSafely(dir) {
77
+ let entries;
78
+ try {
79
+ entries = await fs.readdir(dir, { withFileTypes: true });
80
+ }
81
+ catch (err) {
82
+ if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
83
+ return [];
84
+ }
85
+ throw err;
86
+ }
87
+ return entries
88
+ .filter(entry => entry.isDirectory())
89
+ .map(dir => dir.name);
90
+ }
91
+ //# sourceMappingURL=prune.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Prune unused packages from the global virtual store using mark-and-sweep:
3
+ * 1. Get all registered projects
4
+ * 2. Find all node_modules directories in each project (including workspace packages)
5
+ * 3. Walk symlinks from each node_modules to mark reachable packages
6
+ * 4. Remove any package directories that weren't marked as reachable
7
+ */
8
+ export declare function pruneGlobalVirtualStore(storeDir: string): Promise<void>;
@@ -0,0 +1,245 @@
1
+ import crypto from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import util from 'node:util';
5
+ import { globalInfo } from '@pnpm/logger';
6
+ import { rimraf } from '@zkochan/rimraf';
7
+ import { isSubdir } from 'is-subdir';
8
+ import { getRegisteredProjects } from './projectRegistry.js';
9
+ const LINKS_DIR = 'links';
10
+ /**
11
+ * Prune unused packages from the global virtual store using mark-and-sweep:
12
+ * 1. Get all registered projects
13
+ * 2. Find all node_modules directories in each project (including workspace packages)
14
+ * 3. Walk symlinks from each node_modules to mark reachable packages
15
+ * 4. Remove any package directories that weren't marked as reachable
16
+ */
17
+ export async function pruneGlobalVirtualStore(storeDir) {
18
+ const linksDir = path.join(storeDir, LINKS_DIR);
19
+ if (!await pathExists(linksDir)) {
20
+ return;
21
+ }
22
+ const projects = await getRegisteredProjects(storeDir);
23
+ if (projects.length === 0) {
24
+ globalInfo('No registered projects for global virtual store');
25
+ return;
26
+ }
27
+ globalInfo(`Checking ${projects.length} registered project(s) for global virtual store usage`);
28
+ // Mark phase: collect all reachable package directories
29
+ const reachable = new Set();
30
+ const visited = new Set(); // Track visited directories to prevent infinite loops
31
+ // For each project, find all node_modules directories (root + workspace packages)
32
+ await Promise.all(projects.map(async (projectDir) => {
33
+ const nodeModulesDirs = await findAllNodeModulesDirs(projectDir);
34
+ await Promise.all(nodeModulesDirs.map((modulesDir) => walkSymlinksToStore(modulesDir, linksDir, reachable, visited)));
35
+ }));
36
+ // Sweep phase: remove unreachable packages
37
+ const unreachableCount = await removeUnreachablePackages(linksDir, reachable);
38
+ if (unreachableCount > 0) {
39
+ globalInfo(`Removed ${unreachableCount} package${unreachableCount === 1 ? '' : 's'} from global virtual store`);
40
+ }
41
+ else {
42
+ globalInfo('No unused packages found in global virtual store');
43
+ }
44
+ }
45
+ /**
46
+ * Find all node_modules directories within a project, including those
47
+ * in workspace packages. Does not descend into node_modules directories.
48
+ */
49
+ async function findAllNodeModulesDirs(projectDir) {
50
+ const nodeModulesDirs = [];
51
+ async function scan(dir) {
52
+ let entries;
53
+ try {
54
+ entries = await fs.readdir(dir, { withFileTypes: true });
55
+ }
56
+ catch {
57
+ return;
58
+ }
59
+ const subdirs = [];
60
+ for (const entry of entries) {
61
+ if (!entry.isDirectory())
62
+ continue;
63
+ const entryPath = path.join(dir, entry.name);
64
+ if (entry.name === 'node_modules') {
65
+ nodeModulesDirs.push(entryPath);
66
+ // Don't descend into node_modules
67
+ }
68
+ else if (!entry.name.startsWith('.')) {
69
+ // Collect directories to descend into (workspace packages, etc.)
70
+ // Skip hidden directories like .git, .pnpm
71
+ subdirs.push(entryPath);
72
+ }
73
+ }
74
+ // Scan subdirectories concurrently
75
+ await Promise.all(subdirs.map((subdir) => scan(subdir)));
76
+ }
77
+ await scan(projectDir);
78
+ return nodeModulesDirs;
79
+ }
80
+ /**
81
+ * Recursively walk symlinks from a directory, marking any that point
82
+ * into the global virtual store's links directory.
83
+ */
84
+ async function walkSymlinksToStore(dir, linksDir, reachable, visited) {
85
+ // Prevent infinite loops from circular symlinks
86
+ const dirHash = await getRealPathHash(dir);
87
+ if (visited.has(dirHash)) {
88
+ return;
89
+ }
90
+ visited.add(dirHash);
91
+ let entries;
92
+ try {
93
+ entries = await fs.readdir(dir, { withFileTypes: true });
94
+ }
95
+ catch {
96
+ return;
97
+ }
98
+ await Promise.all(entries.map(async (entry) => {
99
+ const entryPath = path.join(dir, entry.name);
100
+ if (entry.isSymbolicLink()) {
101
+ try {
102
+ const target = await fs.readlink(entryPath);
103
+ const absoluteTarget = path.isAbsolute(target)
104
+ ? target
105
+ : path.resolve(dir, target);
106
+ // Check if this symlink points into the global virtual store
107
+ if (isSubdir(linksDir, absoluteTarget)) {
108
+ // Mark the package directory as reachable
109
+ // The path structure is:
110
+ // - Scoped: {linksDir}/{scope}/{pkgName}/{version}/{hash}/node_modules/{pkgName}
111
+ // - Unscoped: {linksDir}/@/{pkgName}/{version}/{hash}/node_modules/{pkgName}
112
+ // We want to mark the {hash} directory
113
+ const relPath = path.relative(linksDir, absoluteTarget);
114
+ const parts = relPath.split(path.sep);
115
+ // Find the hash directory (the one containing node_modules)
116
+ const nodeModulesIdx = parts.indexOf('node_modules');
117
+ if (nodeModulesIdx !== -1) {
118
+ // Store relative path like "@scope/pkg-a/1.0.0/hash123" or "@/pkg-a/1.0.0/hash123"
119
+ const relativePath = parts.slice(0, nodeModulesIdx).join(path.sep);
120
+ reachable.add(relativePath);
121
+ // Also walk into the package's node_modules for transitive deps
122
+ const pkgNodeModules = path.join(linksDir, relativePath, 'node_modules');
123
+ await walkSymlinksToStore(pkgNodeModules, linksDir, reachable, visited);
124
+ }
125
+ }
126
+ }
127
+ catch {
128
+ // Ignore broken symlinks
129
+ }
130
+ }
131
+ else if (entry.isDirectory() && entry.name !== '.pnpm') {
132
+ // Recurse into directories (but not .pnpm which is the local virtual store)
133
+ await walkSymlinksToStore(entryPath, linksDir, reachable, visited);
134
+ }
135
+ }));
136
+ }
137
+ /**
138
+ * Resolve symlinks and return a hash of the real path (for cycle detection)
139
+ */
140
+ async function getRealPathHash(p) {
141
+ let realPath;
142
+ try {
143
+ realPath = await fs.realpath(p);
144
+ }
145
+ catch {
146
+ realPath = p;
147
+ }
148
+ // Create a compact hash for in-memory use (base64url is shorter than hex that we use for file name hashes)
149
+ return crypto.createHash('sha256').update(realPath).digest('base64url');
150
+ }
151
+ /**
152
+ * Remove package directories from the global virtual store that are not in the reachable set.
153
+ * Returns the count of removed packages.
154
+ *
155
+ * Directory structure is uniform 4-level:
156
+ * - Scoped: {linksDir}/{scope}/{pkgName}/{version}/{hash}/
157
+ * - Unscoped: {linksDir}/@/{pkgName}/{version}/{hash}/
158
+ */
159
+ async function removeUnreachablePackages(linksDir, reachable) {
160
+ // First level is always a scope (either @scope or @ for unscoped packages)
161
+ const scopes = await getSubdirsSafely(linksDir);
162
+ let count = 0;
163
+ await Promise.all(scopes.map(async (scope) => {
164
+ const scopePath = path.join(linksDir, scope);
165
+ const pkgNames = await getSubdirsSafely(scopePath);
166
+ let removedPkgs = 0;
167
+ await Promise.all(pkgNames.map(async (pkgName) => {
168
+ const pkgDir = path.join(scopePath, pkgName);
169
+ const removedVersions = await removeUnreachableVersions(pkgDir, path.join(scope, pkgName), reachable);
170
+ count += removedVersions.count;
171
+ if (removedVersions.allRemoved) {
172
+ // Remove the package directory when all its versions are removed
173
+ await rimraf(pkgDir);
174
+ removedPkgs++;
175
+ }
176
+ }));
177
+ // If we removed all packages in scope, remove the scope directory
178
+ if (removedPkgs === pkgNames.length && pkgNames.length > 0) {
179
+ await rimraf(scopePath);
180
+ }
181
+ }));
182
+ return count;
183
+ }
184
+ /**
185
+ * Remove unreachable versions and hashes for a package.
186
+ * Returns the count of removed packages and whether all versions were removed.
187
+ */
188
+ async function removeUnreachableVersions(pkgDir, pkgPath, // relative path like "@/is-positive" or "@pnpm.e2e/romeo"
189
+ reachable) {
190
+ const versions = await getSubdirsSafely(pkgDir);
191
+ let count = 0;
192
+ let removedVersions = 0;
193
+ await Promise.all(versions.map(async (version) => {
194
+ const versionDir = path.join(pkgDir, version);
195
+ const hashes = await getSubdirsSafely(versionDir);
196
+ // Remove unreachable hash directories
197
+ let removedHashes = 0;
198
+ await Promise.all(hashes.map(async (hash) => {
199
+ const relativePath = path.join(pkgPath, version, hash);
200
+ if (!reachable.has(relativePath)) {
201
+ await rimraf(path.join(versionDir, hash));
202
+ removedHashes++;
203
+ count++;
204
+ }
205
+ }));
206
+ // If we removed all hashes, remove the version directory
207
+ if (removedHashes === hashes.length && hashes.length > 0) {
208
+ await rimraf(versionDir);
209
+ removedVersions++;
210
+ }
211
+ }));
212
+ return {
213
+ count,
214
+ allRemoved: removedVersions === versions.length && versions.length > 0,
215
+ };
216
+ }
217
+ async function pathExists(p) {
218
+ try {
219
+ await fs.stat(p);
220
+ return true;
221
+ }
222
+ catch {
223
+ return false;
224
+ }
225
+ }
226
+ async function getSubdirsSafely(dir) {
227
+ let entries;
228
+ try {
229
+ entries = await fs.readdir(dir, { withFileTypes: true });
230
+ }
231
+ catch (err) {
232
+ if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
233
+ return [];
234
+ }
235
+ throw err;
236
+ }
237
+ const subdirs = [];
238
+ for (const entry of entries) {
239
+ if (entry.isDirectory()) {
240
+ subdirs.push(entry.name);
241
+ }
242
+ }
243
+ return subdirs;
244
+ }
245
+ //# sourceMappingURL=pruneGlobalVirtualStore.js.map
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@pnpm/store.controller",
3
+ "version": "1004.0.0",
4
+ "description": "A storage for packages",
5
+ "keywords": [
6
+ "pnpm",
7
+ "pnpm11",
8
+ "cache",
9
+ "central storage",
10
+ "global store",
11
+ "maching store",
12
+ "packages",
13
+ "storage",
14
+ "store"
15
+ ],
16
+ "license": "MIT",
17
+ "funding": "https://opencollective.com/pnpm",
18
+ "repository": "https://github.com/pnpm/pnpm/tree/main/store/controller",
19
+ "homepage": "https://github.com/pnpm/pnpm/tree/main/store/controller#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/pnpm/pnpm/issues"
22
+ },
23
+ "type": "module",
24
+ "main": "lib/index.js",
25
+ "types": "lib/index.d.ts",
26
+ "exports": {
27
+ ".": "./lib/index.js"
28
+ },
29
+ "files": [
30
+ "lib",
31
+ "!*.map"
32
+ ],
33
+ "directories": {
34
+ "test": "test"
35
+ },
36
+ "dependencies": {
37
+ "@zkochan/rimraf": "^4.0.0",
38
+ "is-subdir": "^2.0.0",
39
+ "ramda": "npm:@pnpm/ramda@0.28.1",
40
+ "ssri": "13.0.0",
41
+ "symlink-dir": "^7.0.0",
42
+ "@pnpm/fetching.fetcher-base": "1001.0.2",
43
+ "@pnpm/error": "1000.0.5",
44
+ "@pnpm/installing.package-requester": "1008.0.0",
45
+ "@pnpm/hooks.types": "1001.0.12",
46
+ "@pnpm/resolving.resolver-base": "1005.1.0",
47
+ "@pnpm/store.cafs": "1000.0.19",
48
+ "@pnpm/store.controller-types": "1004.1.0",
49
+ "@pnpm/store.create-cafs-store": "1000.0.20",
50
+ "@pnpm/store.index": "1000.0.0-0",
51
+ "@pnpm/crypto.hash": "1000.2.1",
52
+ "@pnpm/types": "1000.9.0"
53
+ },
54
+ "peerDependencies": {
55
+ "@pnpm/logger": ">=1001.0.0 <1002.0.0",
56
+ "@pnpm/worker": "^1000.3.0"
57
+ },
58
+ "devDependencies": {
59
+ "@types/ramda": "0.29.12",
60
+ "@types/ssri": "^7.1.5",
61
+ "tempy": "3.0.0",
62
+ "@pnpm/installing.client": "1001.1.4",
63
+ "@pnpm/logger": "1001.0.1",
64
+ "@pnpm/store.controller": "1004.0.0",
65
+ "@pnpm/prepare": "1000.0.4"
66
+ },
67
+ "engines": {
68
+ "node": ">=22.13"
69
+ },
70
+ "jest": {
71
+ "preset": "@pnpm/jest-config"
72
+ },
73
+ "scripts": {
74
+ "start": "tsgo --watch",
75
+ "fix": "tslint -c tslint.json src/**/*.ts test/**/*.ts --fix",
76
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
77
+ "pretest": "rimraf .tmp",
78
+ "_test": "pnpm pretest && cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest",
79
+ "test": "pnpm run compile && pnpm run _test",
80
+ "compile": "tsgo --build && pnpm run lint --fix"
81
+ }
82
+ }