@pnpm/workspace.injected-deps-syncer 1100.0.32 → 1100.0.34
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/CHANGELOG.md +37 -0
- package/lib/DirPatcher.d.ts +8 -2
- package/lib/DirPatcher.js +48 -22
- package/lib/index.d.ts +8 -0
- package/lib/index.js +47 -9
- package/package.json +14 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @pnpm/workspace.injected-deps-syncer
|
|
2
2
|
|
|
3
|
+
## 1100.0.34
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies:
|
|
8
|
+
- @pnpm/bins.linker@1100.0.28
|
|
9
|
+
- @pnpm/bins.remover@1100.0.21
|
|
10
|
+
- @pnpm/bins.resolver@1100.0.15
|
|
11
|
+
- @pnpm/error@1100.1.3
|
|
12
|
+
- @pnpm/fetching.directory-fetcher@1100.0.30
|
|
13
|
+
- @pnpm/installing.modules-yaml@1101.0.0
|
|
14
|
+
- @pnpm/pkg-manifest.reader@1100.0.17
|
|
15
|
+
- @pnpm/types@1102.0.0
|
|
16
|
+
- @pnpm/workspace.projects-reader@1101.0.24
|
|
17
|
+
|
|
18
|
+
## 1100.0.33
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- `syncInjectedDepsAfterScripts` no longer fails with `ERR_PNPM_UNSUPPORTED_INODE_TYPE` when a workspace package contains an inode that is neither a file nor a directory, such as the FIFO 1Password's environments create for `.env`. Such an inode cannot be hardlinked into the injected copy, so it is skipped and the rest of the package still syncs [#13550](https://github.com/pnpm/pnpm/issues/13550).
|
|
23
|
+
|
|
24
|
+
`syncInjectedDepsAfterScripts` also no longer fails with `EEXIST` when a workspace package replaced a file with a directory of the same name since the injected copy was last synced.
|
|
25
|
+
|
|
26
|
+
- `syncInjectedDepsAfterScripts` no longer fails with `ENOTDIR` when a workspace package replaced a directory with a file of the same name and the injected copy still held that directory's contents.
|
|
27
|
+
|
|
28
|
+
- `syncInjectedDepsAfterScripts` now removes the bin link of a bin the script dropped. Previously only new bins were linked, so a build step that stopped declaring one left its shim behind, pointing at a command that was no longer there.
|
|
29
|
+
|
|
30
|
+
- `syncInjectedDepsAfterScripts` now identifies a file by its device as well as its inode number. An inode number is only unique within one filesystem, so on its own it could match an unrelated file on another device and leave that path stale in the injected copy.
|
|
31
|
+
|
|
32
|
+
- Updated dependencies:
|
|
33
|
+
- @pnpm/bins.linker@1100.0.27
|
|
34
|
+
- @pnpm/bins.remover@1100.0.20
|
|
35
|
+
- @pnpm/error@1100.1.2
|
|
36
|
+
- @pnpm/fetching.directory-fetcher@1100.0.29
|
|
37
|
+
- @pnpm/pkg-manifest.reader@1100.0.16
|
|
38
|
+
- @pnpm/workspace.projects-reader@1101.0.23
|
|
39
|
+
|
|
3
40
|
## 1100.0.32
|
|
4
41
|
|
|
5
42
|
### Patch Changes
|
package/lib/DirPatcher.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
export declare const DIR: unique symbol;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* A file's identity, as `<device>:<inode>`. An inode number is only unique
|
|
5
|
+
* within one filesystem, so the device it came from is part of the identity:
|
|
6
|
+
* without it two unrelated files on different devices can collide and be
|
|
7
|
+
* taken for the same file, leaving the injected copy stale.
|
|
8
|
+
*/
|
|
9
|
+
export type File = string;
|
|
4
10
|
export type Dir = typeof DIR;
|
|
5
11
|
export type Value = File | Dir;
|
|
6
12
|
export type InodeMap = Record<string, Value>;
|
|
@@ -43,7 +49,7 @@ export declare function diffDir(oldIndex: InodeMap, newIndex: InodeMap): DirDiff
|
|
|
43
49
|
* The {@link optimizedDirPatch} is assumed to be already optimized (i.e. `removed` is already reversed).
|
|
44
50
|
*/
|
|
45
51
|
export declare function applyPatch(optimizedDirPatch: DirDiff, sourceDir: string, targetDir: string): Promise<void>;
|
|
46
|
-
export type ExtendFilesMapStats = Pick<fs.Stats, 'ino' | 'isFile' | 'isDirectory'>;
|
|
52
|
+
export type ExtendFilesMapStats = Pick<fs.Stats, 'dev' | 'ino' | 'isFile' | 'isDirectory'>;
|
|
47
53
|
export interface ExtendFilesMapOptions {
|
|
48
54
|
/** Map relative path of each file to their real path */
|
|
49
55
|
filesMap: Map<string, string>;
|
package/lib/DirPatcher.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import util from 'node:util';
|
|
4
|
-
import { PnpmError } from '@pnpm/error';
|
|
5
4
|
import { fetchFromDir } from '@pnpm/fetching.directory-fetcher';
|
|
6
5
|
export const DIR = Symbol('Path is a directory');
|
|
7
6
|
// length comparison should place every directory before the files it contains because
|
|
@@ -36,16 +35,34 @@ export function diffDir(oldIndex, newIndex) {
|
|
|
36
35
|
export async function applyPatch(optimizedDirPatch, sourceDir, targetDir) {
|
|
37
36
|
async function addRecursive(sourcePath, targetPath, value) {
|
|
38
37
|
if (value === DIR) {
|
|
39
|
-
await fs.promises.mkdir(targetPath, { recursive: true });
|
|
38
|
+
await retryOverBlockingInode(targetPath, async () => fs.promises.mkdir(targetPath, { recursive: true }));
|
|
40
39
|
}
|
|
41
|
-
else if (typeof value === '
|
|
40
|
+
else if (typeof value === 'string') {
|
|
42
41
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
43
|
-
await fs.promises.link(sourcePath, targetPath);
|
|
42
|
+
await retryOverBlockingInode(targetPath, async () => fs.promises.link(sourcePath, targetPath));
|
|
44
43
|
}
|
|
45
44
|
else {
|
|
46
45
|
const _ = value; // static type guard
|
|
47
46
|
}
|
|
48
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The target may hold an inode that {@link extendFilesMap} skips — a FIFO, a
|
|
50
|
+
* socket, a device. The diff cannot see it, so it is never scheduled for
|
|
51
|
+
* removal, and adding over it fails with `EEXIST`. Clear that path and retry
|
|
52
|
+
* once instead of aborting the sync partway through.
|
|
53
|
+
*/
|
|
54
|
+
async function retryOverBlockingInode(targetPath, add) {
|
|
55
|
+
try {
|
|
56
|
+
await add();
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (!util.types.isNativeError(error) || !('code' in error) || (error.code !== 'EEXIST')) {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
await removeRecursive(targetPath);
|
|
63
|
+
await add();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
49
66
|
async function removeRecursive(targetPath) {
|
|
50
67
|
try {
|
|
51
68
|
await fs.promises.rm(targetPath, { recursive: true, force: true });
|
|
@@ -56,24 +73,33 @@ export async function applyPatch(optimizedDirPatch, sourceDir, targetDir) {
|
|
|
56
73
|
}
|
|
57
74
|
}
|
|
58
75
|
}
|
|
59
|
-
|
|
60
|
-
const sourcePath = path.join(sourceDir, item.path);
|
|
61
|
-
const targetPath = path.join(targetDir, item.path);
|
|
62
|
-
await addRecursive(sourcePath, targetPath, item.newValue);
|
|
63
|
-
}));
|
|
64
|
-
const removing = Promise.all(optimizedDirPatch.removed.map(async (item) => {
|
|
65
|
-
const targetPath = path.join(targetDir, item.path);
|
|
66
|
-
await removeRecursive(targetPath);
|
|
67
|
-
}));
|
|
68
|
-
const modifying = Promise.all(optimizedDirPatch.modified.map(async (item) => {
|
|
76
|
+
async function applyChange(item) {
|
|
69
77
|
const sourcePath = path.join(sourceDir, item.path);
|
|
70
78
|
const targetPath = path.join(targetDir, item.path);
|
|
71
|
-
if (item.oldValue
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
if (item.oldValue !== undefined) {
|
|
80
|
+
await removeRecursive(targetPath);
|
|
81
|
+
}
|
|
74
82
|
await addRecursive(sourcePath, targetPath, item.newValue);
|
|
83
|
+
}
|
|
84
|
+
const changes = [...optimizedDirPatch.added, ...optimizedDirPatch.modified]
|
|
85
|
+
.filter(item => item.oldValue !== item.newValue);
|
|
86
|
+
const newDirs = changes.filter(item => item.newValue === DIR).sort((a, b) => comparePaths(a.path, b.path));
|
|
87
|
+
const newFiles = changes.filter(item => item.newValue !== DIR);
|
|
88
|
+
// The phase order is load-bearing twice over. Removals go first, so a path
|
|
89
|
+
// the source turned from a directory into a file still has a directory in it
|
|
90
|
+
// when its dropped children are unlinked. Directories then go in ahead of the
|
|
91
|
+
// files they hold, so a directory is always empty when it displaces what the
|
|
92
|
+
// target held at its path — otherwise a removal landing late would take out
|
|
93
|
+
// files a sibling had already linked. A path the target holds as a file and
|
|
94
|
+
// the source as a directory lands in `modified` rather than `added`, so both
|
|
95
|
+
// arrays feed the directory pass.
|
|
96
|
+
await Promise.all(optimizedDirPatch.removed.map(async (item) => {
|
|
97
|
+
await removeRecursive(path.join(targetDir, item.path));
|
|
75
98
|
}));
|
|
76
|
-
|
|
99
|
+
for (const item of newDirs) {
|
|
100
|
+
await applyChange(item); // eslint-disable-line no-await-in-loop
|
|
101
|
+
}
|
|
102
|
+
await Promise.all(newFiles.map(applyChange));
|
|
77
103
|
}
|
|
78
104
|
/**
|
|
79
105
|
* Convert a pair of a files index map, which is a map from relative path of each file to their real paths,
|
|
@@ -93,17 +119,17 @@ export async function extendFilesMap({ filesMap, filesStats }) {
|
|
|
93
119
|
await Promise.all(Array.from(filesMap.entries()).map(async ([relativePath, realPath]) => {
|
|
94
120
|
const stats = filesStats?.[relativePath] ?? await fs.promises.stat(realPath);
|
|
95
121
|
if (stats.isFile()) {
|
|
96
|
-
addInodeAndAncestors(relativePath, stats
|
|
122
|
+
addInodeAndAncestors(relativePath, fileId(stats));
|
|
97
123
|
}
|
|
98
124
|
else if (stats.isDirectory()) {
|
|
99
125
|
addInodeAndAncestors(relativePath, DIR);
|
|
100
126
|
}
|
|
101
|
-
else
|
|
102
|
-
|
|
103
|
-
}
|
|
127
|
+
// Anything else — a FIFO, a socket, a device — cannot be hardlinked into
|
|
128
|
+
// the injected copy, so it is left out of the map.
|
|
104
129
|
}));
|
|
105
130
|
return result;
|
|
106
131
|
}
|
|
132
|
+
const fileId = (stats) => `${stats.dev}:${stats.ino}`;
|
|
107
133
|
export class DirPatcher {
|
|
108
134
|
sourceDir;
|
|
109
135
|
targetDir;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import type { DependencyManifest } from '@pnpm/types';
|
|
1
2
|
export interface SyncInjectedDepsOptions {
|
|
2
3
|
pkgName: string | undefined;
|
|
3
4
|
pkgRootDir: string;
|
|
4
5
|
workspaceDir: string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* The package's manifest as it was before the scripts ran. A script that
|
|
8
|
+
* drops a bin leaves its shim behind, and the copies cannot say which bins
|
|
9
|
+
* they used to have: their `package.json` is hardlinked to the source, so
|
|
10
|
+
* an in-place rewrite has already reached them.
|
|
11
|
+
*/
|
|
12
|
+
manifestBeforeScripts?: DependencyManifest;
|
|
5
13
|
}
|
|
6
14
|
export declare function syncInjectedDeps(opts: SyncInjectedDepsOptions): Promise<void>;
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { linkBins, linkBinsOfPackages } from '@pnpm/bins.linker';
|
|
3
|
+
import { removeBin } from '@pnpm/bins.remover';
|
|
4
|
+
import { getBinsFromPackageManifest } from '@pnpm/bins.resolver';
|
|
3
5
|
import { PnpmError } from '@pnpm/error';
|
|
4
6
|
import { readModulesManifest } from '@pnpm/installing.modules-yaml';
|
|
5
7
|
import { logger as createLogger } from '@pnpm/logger';
|
|
@@ -41,21 +43,53 @@ export async function syncInjectedDeps(opts) {
|
|
|
41
43
|
});
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
|
-
const
|
|
46
|
+
const resolvedTargetDirs = targetDirs.map(targetDir => path.resolve(opts.workspaceDir, targetDir));
|
|
47
|
+
const patchers = await DirPatcher.fromMultipleTargets(pkgRootDir, resolvedTargetDirs);
|
|
45
48
|
await Promise.all(patchers.map(patcher => patcher.apply()));
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
await syncBinLinks({
|
|
50
|
+
// The install hoists bins into the virtual store's own `.bin` as well.
|
|
51
|
+
hoistedBinDir: modules.virtualStoreDir == null
|
|
52
|
+
? undefined
|
|
53
|
+
: path.join(path.resolve(opts.workspaceDir, modules.virtualStoreDir), 'node_modules', '.bin'),
|
|
54
|
+
pkgRootDir,
|
|
55
|
+
previousBinNames: opts.manifestBeforeScripts == null
|
|
56
|
+
? []
|
|
57
|
+
: (await getBinsFromPackageManifest(opts.manifestBeforeScripts, pkgRootDir)).map(command => command.name),
|
|
58
|
+
resolvedTargetDirs,
|
|
59
|
+
workspaceDir: opts.workspaceDir,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** The commands a package declares, or none when it declares no bins. */
|
|
63
|
+
async function readBinNames(pkgDir) {
|
|
64
|
+
const manifest = await safeReadPackageJsonFromDir(pkgDir);
|
|
65
|
+
if (!manifest?.name)
|
|
66
|
+
return [];
|
|
67
|
+
const commands = await getBinsFromPackageManifest(manifest, pkgDir);
|
|
68
|
+
return commands.map(command => command.name);
|
|
48
69
|
}
|
|
49
|
-
async function syncBinLinks(
|
|
50
|
-
const manifest = await safeReadPackageJsonFromDir(pkgRootDir);
|
|
51
|
-
if (!manifest?.
|
|
70
|
+
async function syncBinLinks(opts) {
|
|
71
|
+
const manifest = await safeReadPackageJsonFromDir(opts.pkgRootDir);
|
|
72
|
+
if (!manifest?.name) {
|
|
52
73
|
return;
|
|
53
74
|
}
|
|
75
|
+
// A script can drop a bin as easily as it can add one. `linkBins` only ever
|
|
76
|
+
// creates shims, so without this the shim for a dropped bin survives and
|
|
77
|
+
// points at a command that is no longer there.
|
|
78
|
+
const currentBinNames = new Set(await readBinNames(opts.pkgRootDir));
|
|
79
|
+
const staleBinNames = opts.previousBinNames.filter(name => !currentBinNames.has(name));
|
|
54
80
|
// Step 1: Link bins in .pnpm virtual store
|
|
55
|
-
const binLinkPromises =
|
|
56
|
-
const resolvedTargetDir = path.resolve(workspaceDir, targetDir);
|
|
81
|
+
const binLinkPromises = opts.resolvedTargetDirs.map(async (resolvedTargetDir) => {
|
|
57
82
|
const parentNodeModulesDir = path.dirname(resolvedTargetDir);
|
|
58
83
|
const binDir = path.join(parentNodeModulesDir, '.bin');
|
|
84
|
+
// The installer writes an injected package's own bins inside the copy,
|
|
85
|
+
// while this function writes them beside it. A dropped bin has to be
|
|
86
|
+
// cleared from both, or the one this function never wrote survives.
|
|
87
|
+
const binDirs = [binDir, path.join(resolvedTargetDir, 'node_modules', '.bin')];
|
|
88
|
+
if (opts.hoistedBinDir != null)
|
|
89
|
+
binDirs.push(opts.hoistedBinDir);
|
|
90
|
+
await Promise.all(binDirs.flatMap(dir => staleBinNames.map(async (name) => removeBin(path.join(dir, name)))));
|
|
91
|
+
if (manifest.bin == null)
|
|
92
|
+
return;
|
|
59
93
|
await linkBinsOfPackages([{
|
|
60
94
|
manifest,
|
|
61
95
|
location: resolvedTargetDir,
|
|
@@ -65,10 +99,14 @@ async function syncBinLinks(pkgRootDir, targetDirs, workspaceDir) {
|
|
|
65
99
|
// We need to relink bins for all workspace projects because injected deps
|
|
66
100
|
// can be used by any project in the workspace. We relink all bins (not just
|
|
67
101
|
// this package) to ensure consistency.
|
|
68
|
-
const allProjects = await findWorkspaceProjectsNoCheck(workspaceDir, {});
|
|
102
|
+
const allProjects = await findWorkspaceProjectsNoCheck(opts.workspaceDir, {});
|
|
69
103
|
const consumerLinkPromises = allProjects.map(async (project) => {
|
|
70
104
|
const projectNodeModules = path.join(project.rootDir, 'node_modules');
|
|
71
105
|
const projectBinDir = path.join(projectNodeModules, '.bin');
|
|
106
|
+
// A stale name another package legitimately owns is put back by the
|
|
107
|
+
// relink below, so removing first costs nothing and catches the shim
|
|
108
|
+
// this package left behind.
|
|
109
|
+
await Promise.all(staleBinNames.map(async (name) => removeBin(path.join(projectBinDir, name))));
|
|
72
110
|
// Relink all bins in the project's node_modules
|
|
73
111
|
await linkBins(projectNodeModules, projectBinDir, {
|
|
74
112
|
allowExoticManifests: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/workspace.injected-deps-syncer",
|
|
3
|
-
"version": "1100.0.
|
|
3
|
+
"version": "1100.0.34",
|
|
4
4
|
"description": "Update all injected replica of a workspace package",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -27,14 +27,16 @@
|
|
|
27
27
|
"!*.map"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@pnpm/bins.linker": "1100.0.
|
|
31
|
-
"@pnpm/
|
|
32
|
-
"@pnpm/
|
|
33
|
-
"@pnpm/
|
|
34
|
-
"@pnpm/
|
|
30
|
+
"@pnpm/bins.linker": "1100.0.28",
|
|
31
|
+
"@pnpm/bins.remover": "1100.0.21",
|
|
32
|
+
"@pnpm/bins.resolver": "1100.0.15",
|
|
33
|
+
"@pnpm/error": "1100.1.3",
|
|
34
|
+
"@pnpm/fetching.directory-fetcher": "1100.0.30",
|
|
35
|
+
"@pnpm/installing.modules-yaml": "1101.0.0",
|
|
36
|
+
"@pnpm/pkg-manifest.reader": "1100.0.17",
|
|
35
37
|
"@pnpm/text.ordinal-comparator": "1100.0.0",
|
|
36
|
-
"@pnpm/types": "
|
|
37
|
-
"@pnpm/workspace.projects-reader": "1101.0.
|
|
38
|
+
"@pnpm/types": "1102.0.0",
|
|
39
|
+
"@pnpm/workspace.projects-reader": "1101.0.24",
|
|
38
40
|
"@types/normalize-path": "^3.0.2",
|
|
39
41
|
"normalize-path": "^3.0.0"
|
|
40
42
|
},
|
|
@@ -44,8 +46,10 @@
|
|
|
44
46
|
"devDependencies": {
|
|
45
47
|
"@jest/globals": "30.4.1",
|
|
46
48
|
"@pnpm/logger": "1100.0.0",
|
|
47
|
-
"@pnpm/prepare": "1100.0.
|
|
48
|
-
"@pnpm/workspace.injected-deps-syncer": "1100.0.
|
|
49
|
+
"@pnpm/prepare": "1100.0.27",
|
|
50
|
+
"@pnpm/workspace.injected-deps-syncer": "1100.0.34",
|
|
51
|
+
"@types/is-windows": "^1.0.2",
|
|
52
|
+
"is-windows": "^1.0.2"
|
|
49
53
|
},
|
|
50
54
|
"engines": {
|
|
51
55
|
"node": ">=22.13"
|