@pnpm/lockfile.fs 1100.1.13 → 1100.1.15
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 +28 -0
- package/lib/envLockfile.d.ts +4 -0
- package/lib/envLockfile.js +61 -0
- package/lib/errors/LockfileBreakingChangeError.d.ts +5 -0
- package/lib/errors/LockfileBreakingChangeError.js +9 -0
- package/lib/errors/index.d.ts +1 -0
- package/lib/errors/index.js +2 -0
- package/lib/existsWantedLockfile.d.ts +6 -0
- package/lib/existsWantedLockfile.js +23 -0
- package/lib/getLockfileImporterId.d.ts +2 -0
- package/lib/getLockfileImporterId.js +6 -0
- package/lib/gitBranchLockfile.d.ts +3 -0
- package/lib/gitBranchLockfile.js +22 -0
- package/lib/gitMergeFile.d.ts +3 -0
- package/lib/gitMergeFile.js +47 -0
- package/lib/index.d.ts +10 -0
- package/lib/lockfileFormatConverters.d.ts +3 -0
- package/lib/lockfileFormatConverters.js +227 -0
- package/lib/lockfileName.d.ts +6 -0
- package/lib/lockfileName.js +19 -0
- package/lib/logger.d.ts +1 -0
- package/lib/logger.js +3 -0
- package/lib/read.d.ts +42 -0
- package/lib/read.js +203 -0
- package/lib/sortLockfileKeys.d.ts +3 -0
- package/lib/sortLockfileKeys.js +78 -0
- package/lib/write.d.ts +39 -0
- package/lib/write.js +203 -0
- package/lib/yamlDocuments.d.ts +27 -0
- package/lib/yamlDocuments.js +165 -0
- package/package.json +13 -13
package/lib/read.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs, { promises as fsp } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import util from 'node:util';
|
|
4
|
+
import { LOCKFILE_VERSION, WANTED_LOCKFILE, } from '@pnpm/constants';
|
|
5
|
+
import { PnpmError } from '@pnpm/error';
|
|
6
|
+
import { mergeLockfileChanges } from '@pnpm/lockfile.merger';
|
|
7
|
+
import { comverToSemver } from 'comver-to-semver';
|
|
8
|
+
import yaml from 'js-yaml';
|
|
9
|
+
import semver from 'semver';
|
|
10
|
+
import stripBom from 'strip-bom';
|
|
11
|
+
import { LockfileBreakingChangeError } from './errors/index.js';
|
|
12
|
+
import { getGitBranchLockfileNames } from './gitBranchLockfile.js';
|
|
13
|
+
import { autofixMergeConflicts, isDiff } from './gitMergeFile.js';
|
|
14
|
+
import { convertToLockfileFile, convertToLockfileObject } from './lockfileFormatConverters.js';
|
|
15
|
+
import { getWantedLockfileName } from './lockfileName.js';
|
|
16
|
+
import { lockfileLogger as logger } from './logger.js';
|
|
17
|
+
import { extractMainDocument } from './yamlDocuments.js';
|
|
18
|
+
export async function readCurrentLockfile(pnpmInternalDir, opts) {
|
|
19
|
+
const lockfilePath = path.join(pnpmInternalDir, 'lock.yaml');
|
|
20
|
+
return (await _read(lockfilePath, pnpmInternalDir, opts)).lockfile;
|
|
21
|
+
}
|
|
22
|
+
export async function readWantedLockfileAndAutofixConflicts(pkgPath, opts) {
|
|
23
|
+
return _readWantedLockfile(pkgPath, {
|
|
24
|
+
...opts,
|
|
25
|
+
autofixMergeConflicts: true,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function readWantedLockfile(pkgPath, opts) {
|
|
29
|
+
return (await _readWantedLockfile(pkgPath, opts)).lockfile;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read the wanted lockfile in its on-disk shape ({@link LockfileFile}),
|
|
33
|
+
* skipping the conversion to the in-memory {@link LockfileObject}.
|
|
34
|
+
*
|
|
35
|
+
* Use this when the caller needs the exact serialized form — e.g. to
|
|
36
|
+
* forward the lockfile to a server that speaks the on-disk format —
|
|
37
|
+
* rather than the in-process representation.
|
|
38
|
+
*/
|
|
39
|
+
export async function readWantedLockfileFile(pkgPath, opts) {
|
|
40
|
+
return (await _readWantedLockfile(pkgPath, opts)).lockfileFile;
|
|
41
|
+
}
|
|
42
|
+
export function wantedLockfileHasMergeConflictsSync(pkgPath, lockfileName = WANTED_LOCKFILE) {
|
|
43
|
+
try {
|
|
44
|
+
const lockfileRawContent = stripBom(fs.readFileSync(path.join(pkgPath, lockfileName), 'utf8'));
|
|
45
|
+
return isDiff(extractMainDocument(lockfileRawContent));
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function _read(lockfilePath, prefix, // only for logging
|
|
55
|
+
opts) {
|
|
56
|
+
let lockfileRawContent;
|
|
57
|
+
try {
|
|
58
|
+
lockfileRawContent = stripBom(await fsp.readFile(lockfilePath, 'utf8'));
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (!(util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT')) {
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
lockfile: null,
|
|
66
|
+
lockfileFile: null,
|
|
67
|
+
hadConflicts: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// Skip the env lockfile document if present (first document in combined format)
|
|
71
|
+
lockfileRawContent = extractMainDocument(lockfileRawContent);
|
|
72
|
+
if (!lockfileRawContent.trim()) {
|
|
73
|
+
return {
|
|
74
|
+
lockfile: null,
|
|
75
|
+
lockfileFile: null,
|
|
76
|
+
hadConflicts: false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
let lockfile;
|
|
80
|
+
let lockfileFile;
|
|
81
|
+
let hadConflicts;
|
|
82
|
+
try {
|
|
83
|
+
lockfileFile = yaml.load(lockfileRawContent);
|
|
84
|
+
lockfile = convertToLockfileObject(lockfileFile);
|
|
85
|
+
hadConflicts = false;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (!opts.autofixMergeConflicts || !isDiff(lockfileRawContent)) {
|
|
89
|
+
throw new PnpmError('BROKEN_LOCKFILE', `The lockfile at "${lockfilePath}" is broken: ${formatLockfileError(err)}`);
|
|
90
|
+
}
|
|
91
|
+
hadConflicts = true;
|
|
92
|
+
lockfile = autofixMergeConflicts(lockfileRawContent);
|
|
93
|
+
lockfileFile = convertToLockfileFile(lockfile);
|
|
94
|
+
logger.info({
|
|
95
|
+
message: `Merge conflict detected in ${WANTED_LOCKFILE} and successfully merged`,
|
|
96
|
+
prefix,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (lockfile) {
|
|
100
|
+
const lockfileSemver = comverToSemver((lockfile.lockfileVersion ?? 0).toString());
|
|
101
|
+
if (!opts.wantedVersions ||
|
|
102
|
+
opts.wantedVersions.length === 0 ||
|
|
103
|
+
opts.wantedVersions.some((wantedVersion) => {
|
|
104
|
+
if (semver.major(lockfileSemver) !== semver.major(comverToSemver(wantedVersion)))
|
|
105
|
+
return false;
|
|
106
|
+
if (lockfile.lockfileVersion !== '6.1' && semver.gt(lockfileSemver, comverToSemver(wantedVersion))) {
|
|
107
|
+
logger.warn({
|
|
108
|
+
message: `Your ${WANTED_LOCKFILE} was generated by a newer version of pnpm. ` +
|
|
109
|
+
`It is a compatible version but it might get downgraded to version ${wantedVersion}`,
|
|
110
|
+
prefix,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
})) {
|
|
115
|
+
return { lockfile, lockfileFile, hadConflicts };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (opts.ignoreIncompatible) {
|
|
119
|
+
logger.warn({
|
|
120
|
+
message: `Ignoring not compatible lockfile at ${lockfilePath}`,
|
|
121
|
+
prefix,
|
|
122
|
+
});
|
|
123
|
+
return { lockfile: null, lockfileFile: null, hadConflicts: false };
|
|
124
|
+
}
|
|
125
|
+
throw new LockfileBreakingChangeError(lockfilePath);
|
|
126
|
+
}
|
|
127
|
+
function formatLockfileError(err) {
|
|
128
|
+
if (isYamlException(err)) {
|
|
129
|
+
const reason = typeof err.reason === 'string' ? err.reason : 'Unable to parse YAML';
|
|
130
|
+
const line = err.mark?.line;
|
|
131
|
+
const column = err.mark?.column;
|
|
132
|
+
const position = typeof line === 'number' && Number.isFinite(line) &&
|
|
133
|
+
typeof column === 'number' && Number.isFinite(column)
|
|
134
|
+
? ` (${line + 1}:${column + 1})`
|
|
135
|
+
: '';
|
|
136
|
+
return `${reason}${position}`;
|
|
137
|
+
}
|
|
138
|
+
return util.types.isNativeError(err) ? err.message : String(err);
|
|
139
|
+
}
|
|
140
|
+
function isYamlException(err) {
|
|
141
|
+
return typeof err === 'object' && err !== null &&
|
|
142
|
+
'name' in err && err.name === 'YAMLException';
|
|
143
|
+
}
|
|
144
|
+
export function createLockfileObject(importerIds, opts) {
|
|
145
|
+
const importers = {};
|
|
146
|
+
for (const importerId of importerIds) {
|
|
147
|
+
importers[importerId] = {
|
|
148
|
+
dependencies: {},
|
|
149
|
+
specifiers: {},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
importers,
|
|
154
|
+
lockfileVersion: opts.lockfileVersion || LOCKFILE_VERSION,
|
|
155
|
+
settings: {
|
|
156
|
+
autoInstallPeers: opts.autoInstallPeers,
|
|
157
|
+
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
|
|
158
|
+
peersSuffixMaxLength: opts.peersSuffixMaxLength,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function _readWantedLockfile(pkgPath, opts) {
|
|
163
|
+
const lockfileNames = [WANTED_LOCKFILE];
|
|
164
|
+
if (opts.useGitBranchLockfile) {
|
|
165
|
+
const gitBranchLockfileName = await getWantedLockfileName(opts);
|
|
166
|
+
if (gitBranchLockfileName !== WANTED_LOCKFILE) {
|
|
167
|
+
lockfileNames.unshift(gitBranchLockfileName);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
let result = { lockfile: null, lockfileFile: null, hadConflicts: false };
|
|
171
|
+
/* eslint-disable no-await-in-loop */
|
|
172
|
+
for (const lockfileName of lockfileNames) {
|
|
173
|
+
result = await _read(path.join(pkgPath, lockfileName), pkgPath, { ...opts, autofixMergeConflicts: true });
|
|
174
|
+
if (result.lockfile) {
|
|
175
|
+
if (opts.mergeGitBranchLockfiles) {
|
|
176
|
+
result.lockfile = await _mergeGitBranchLockfiles(result.lockfile, pkgPath, pkgPath, opts);
|
|
177
|
+
result.lockfileFile = result.lockfile ? convertToLockfileFile(result.lockfile) : null;
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/* eslint-enable no-await-in-loop */
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
async function _mergeGitBranchLockfiles(lockfile, lockfileDir, prefix, opts) {
|
|
186
|
+
if (!lockfile) {
|
|
187
|
+
return lockfile;
|
|
188
|
+
}
|
|
189
|
+
const gitBranchLockfiles = (await _readGitBranchLockfiles(lockfileDir, prefix, opts)).map(({ lockfile }) => lockfile);
|
|
190
|
+
let mergedLockfile = lockfile;
|
|
191
|
+
for (const gitBranchLockfile of gitBranchLockfiles) {
|
|
192
|
+
if (!gitBranchLockfile) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
mergedLockfile = mergeLockfileChanges(mergedLockfile, gitBranchLockfile);
|
|
196
|
+
}
|
|
197
|
+
return mergedLockfile;
|
|
198
|
+
}
|
|
199
|
+
async function _readGitBranchLockfiles(lockfileDir, prefix, opts) {
|
|
200
|
+
const files = await getGitBranchLockfileNames(lockfileDir);
|
|
201
|
+
return Promise.all(files.map((file) => _read(path.join(lockfileDir, file), prefix, opts)));
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=read.js.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { sortDeepKeys, sortDirectKeys, sortKeysByPriority } from '@pnpm/object.key-sorting';
|
|
2
|
+
const ORDERED_KEYS = {
|
|
3
|
+
resolution: 1,
|
|
4
|
+
id: 2,
|
|
5
|
+
name: 3,
|
|
6
|
+
version: 4,
|
|
7
|
+
engines: 5,
|
|
8
|
+
cpu: 6,
|
|
9
|
+
os: 7,
|
|
10
|
+
libc: 8,
|
|
11
|
+
deprecated: 9,
|
|
12
|
+
hasBin: 10,
|
|
13
|
+
prepare: 11,
|
|
14
|
+
requiresBuild: 12,
|
|
15
|
+
bundleDependencies: 13,
|
|
16
|
+
peerDependencies: 14,
|
|
17
|
+
peerDependenciesMeta: 15,
|
|
18
|
+
dependencies: 16,
|
|
19
|
+
optionalDependencies: 17,
|
|
20
|
+
transitivePeerDependencies: 18,
|
|
21
|
+
dev: 19,
|
|
22
|
+
optional: 20,
|
|
23
|
+
};
|
|
24
|
+
const ROOT_KEYS = [
|
|
25
|
+
'lockfileVersion',
|
|
26
|
+
'settings',
|
|
27
|
+
'catalogs',
|
|
28
|
+
'overrides',
|
|
29
|
+
'packageExtensionsChecksum',
|
|
30
|
+
'pnpmfileChecksum',
|
|
31
|
+
'patchedDependencies',
|
|
32
|
+
'importers',
|
|
33
|
+
'packages',
|
|
34
|
+
];
|
|
35
|
+
const ROOT_KEYS_ORDER = Object.fromEntries(ROOT_KEYS.map((key, index) => [key, index]));
|
|
36
|
+
export function sortLockfileKeys(lockfile) {
|
|
37
|
+
if (lockfile.importers != null) {
|
|
38
|
+
lockfile.importers = sortDirectKeys(lockfile.importers);
|
|
39
|
+
for (const [importerId, importer] of Object.entries(lockfile.importers)) {
|
|
40
|
+
lockfile.importers[importerId] = sortKeysByPriority({
|
|
41
|
+
priority: ROOT_KEYS_ORDER,
|
|
42
|
+
deep: true,
|
|
43
|
+
}, importer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (lockfile.packages != null) {
|
|
47
|
+
lockfile.packages = sortDirectKeys(lockfile.packages);
|
|
48
|
+
for (const [pkgId, pkg] of Object.entries(lockfile.packages)) {
|
|
49
|
+
lockfile.packages[pkgId] = sortKeysByPriority({
|
|
50
|
+
priority: ORDERED_KEYS,
|
|
51
|
+
deep: true,
|
|
52
|
+
}, pkg);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (lockfile.snapshots != null) {
|
|
56
|
+
lockfile.snapshots = sortDirectKeys(lockfile.snapshots);
|
|
57
|
+
for (const [pkgId, pkg] of Object.entries(lockfile.snapshots)) {
|
|
58
|
+
lockfile.snapshots[pkgId] = sortKeysByPriority({
|
|
59
|
+
priority: ORDERED_KEYS,
|
|
60
|
+
deep: true,
|
|
61
|
+
}, pkg);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if ('catalogs' in lockfile && lockfile.catalogs != null) {
|
|
65
|
+
lockfile.catalogs = sortDirectKeys(lockfile.catalogs);
|
|
66
|
+
for (const [catalogName, catalog] of Object.entries(lockfile.catalogs)) {
|
|
67
|
+
lockfile.catalogs[catalogName] = sortDeepKeys(catalog);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if ('time' in lockfile && lockfile.time != null) {
|
|
71
|
+
lockfile.time = sortDirectKeys(lockfile.time);
|
|
72
|
+
}
|
|
73
|
+
if ('patchedDependencies' in lockfile && lockfile.patchedDependencies != null) {
|
|
74
|
+
lockfile.patchedDependencies = sortDirectKeys(lockfile.patchedDependencies);
|
|
75
|
+
}
|
|
76
|
+
return sortKeysByPriority({ priority: ROOT_KEYS_ORDER }, lockfile);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=sortLockfileKeys.js.map
|
package/lib/write.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LockfileFile, LockfileObject } from '@pnpm/lockfile.types';
|
|
2
|
+
export declare function lockfileYamlDump(obj: object): string;
|
|
3
|
+
/**
|
|
4
|
+
* Returns the canonical post-write lockfile — structurally identical
|
|
5
|
+
* to what `readWantedLockfile` would parse back. Lets callers like
|
|
6
|
+
* the verification cache hash the as-saved form without re-reading.
|
|
7
|
+
*/
|
|
8
|
+
export declare function writeWantedLockfile(pkgPath: string, wantedLockfile: LockfileObject, opts?: {
|
|
9
|
+
useGitBranchLockfile?: boolean;
|
|
10
|
+
mergeGitBranchLockfiles?: boolean;
|
|
11
|
+
/** Pre-resolved filename; skips the `getWantedLockfileName` (and
|
|
12
|
+
* its `getCurrentBranch`) call when supplied. */
|
|
13
|
+
lockfileName?: string;
|
|
14
|
+
}): Promise<LockfileObject>;
|
|
15
|
+
export declare function writeCurrentLockfile(virtualStoreDir: string, currentLockfile: LockfileObject): Promise<LockfileObject | undefined>;
|
|
16
|
+
export declare function writeLockfileFile(lockfilePath: string, wantedLockfile: LockfileFile): Promise<void>;
|
|
17
|
+
export declare function isEmptyLockfile(lockfile: LockfileObject): boolean;
|
|
18
|
+
export interface WriteLockfilesResult {
|
|
19
|
+
/**
|
|
20
|
+
* The canonical "as-saved" wanted lockfile — the inverse converter
|
|
21
|
+
* applied to the same object that was serialized to YAML. Hashing
|
|
22
|
+
* this is equivalent to hashing the lockfile the next install will
|
|
23
|
+
* load from disk (modulo undefined values that YAML drops, which any
|
|
24
|
+
* sensible canonicalization-then-hash routine should strip).
|
|
25
|
+
*/
|
|
26
|
+
wantedLockfile: LockfileObject;
|
|
27
|
+
/** Same as above for the current lockfile, or undefined when it was skipped because empty. */
|
|
28
|
+
currentLockfile: LockfileObject | undefined;
|
|
29
|
+
}
|
|
30
|
+
export declare function writeLockfiles(opts: {
|
|
31
|
+
wantedLockfile: LockfileObject;
|
|
32
|
+
wantedLockfileDir: string;
|
|
33
|
+
currentLockfile: LockfileObject;
|
|
34
|
+
currentLockfileDir: string;
|
|
35
|
+
useGitBranchLockfile?: boolean;
|
|
36
|
+
mergeGitBranchLockfiles?: boolean;
|
|
37
|
+
/** See {@link writeWantedLockfile}'s `lockfileName` option. */
|
|
38
|
+
wantedLockfileName?: string;
|
|
39
|
+
}): Promise<WriteLockfilesResult>;
|
package/lib/write.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { WANTED_LOCKFILE } from '@pnpm/constants';
|
|
5
|
+
import { rimraf } from '@zkochan/rimraf';
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import { isEmpty } from 'ramda';
|
|
8
|
+
import writeFileAtomic from 'write-file-atomic';
|
|
9
|
+
import { convertToLockfileFile, convertToLockfileObject } from './lockfileFormatConverters.js';
|
|
10
|
+
import { getWantedLockfileName } from './lockfileName.js';
|
|
11
|
+
import { lockfileLogger as logger } from './logger.js';
|
|
12
|
+
import { sortLockfileKeys } from './sortLockfileKeys.js';
|
|
13
|
+
import { ensureLockfileIsNotSymlink, extractEnvDocument, readLockfileToString, YAML_DOCUMENT_SEPARATOR, YAML_DOCUMENT_START } from './yamlDocuments.js';
|
|
14
|
+
const LOCKFILE_YAML_FORMAT = {
|
|
15
|
+
blankLines: true,
|
|
16
|
+
lineWidth: -1,
|
|
17
|
+
noCompatMode: true,
|
|
18
|
+
noRefs: true,
|
|
19
|
+
sortKeys: false,
|
|
20
|
+
};
|
|
21
|
+
export function lockfileYamlDump(obj) {
|
|
22
|
+
return yaml.dump(obj, LOCKFILE_YAML_FORMAT);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Returns the canonical post-write lockfile — structurally identical
|
|
26
|
+
* to what `readWantedLockfile` would parse back. Lets callers like
|
|
27
|
+
* the verification cache hash the as-saved form without re-reading.
|
|
28
|
+
*/
|
|
29
|
+
export async function writeWantedLockfile(pkgPath, wantedLockfile, opts) {
|
|
30
|
+
const wantedLockfileName = opts?.lockfileName ?? await getWantedLockfileName(opts);
|
|
31
|
+
return writeLockfile(wantedLockfileName, pkgPath, wantedLockfile);
|
|
32
|
+
}
|
|
33
|
+
export async function writeCurrentLockfile(virtualStoreDir, currentLockfile) {
|
|
34
|
+
// empty lockfile is not saved
|
|
35
|
+
if (isEmptyLockfile(currentLockfile)) {
|
|
36
|
+
await rimraf(path.join(virtualStoreDir, 'lock.yaml'));
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
await fs.mkdir(virtualStoreDir, { recursive: true });
|
|
40
|
+
return writeLockfile('lock.yaml', virtualStoreDir, currentLockfile);
|
|
41
|
+
}
|
|
42
|
+
async function writeLockfile(lockfileFilename, pkgPath, wantedLockfile) {
|
|
43
|
+
const lockfilePath = path.join(pkgPath, lockfileFilename);
|
|
44
|
+
const lockfileToStringify = convertToLockfileFile(wantedLockfile);
|
|
45
|
+
const yamlDoc = yamlStringify(lockfileToStringify);
|
|
46
|
+
await writeLockfileDoc(lockfilePath, lockfileFilename, yamlDoc);
|
|
47
|
+
// YAML drops undefined on serialize, so the in-memory LockfileFile
|
|
48
|
+
// can carry fields (like an unset settings.dedupePeers) that won't
|
|
49
|
+
// survive a round-trip; strip them to mirror what the next reader
|
|
50
|
+
// will parse back.
|
|
51
|
+
return convertToLockfileObject(stripUndefinedDeep(lockfileToStringify));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Writes a serialized lockfile, re-reading the env document that leads
|
|
55
|
+
* `pnpm-lock.yaml` to preserve it. Ideally it would be captured during the
|
|
56
|
+
* initial lockfile read and passed through, but that would require threading it
|
|
57
|
+
* through 25+ call sites; re-reading is cheap since the file is likely still in
|
|
58
|
+
* the OS page cache.
|
|
59
|
+
*/
|
|
60
|
+
async function writeLockfileDoc(lockfilePath, lockfileName, mainDoc) {
|
|
61
|
+
if (lockfileName !== WANTED_LOCKFILE) {
|
|
62
|
+
await writeFileAtomic(lockfilePath, mainDoc);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const existing = await readLockfileToString(lockfilePath);
|
|
66
|
+
const envDoc = existing == null ? null : extractEnvDocument(existing);
|
|
67
|
+
const content = envDoc == null
|
|
68
|
+
? mainDoc
|
|
69
|
+
: `${YAML_DOCUMENT_START}${envDoc}${YAML_DOCUMENT_SEPARATOR}${mainDoc}`;
|
|
70
|
+
if (existing === content)
|
|
71
|
+
return;
|
|
72
|
+
await writeWantedLockfileAtomic(lockfilePath, content);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Publishes with `rename`, not the `write-file-atomic` used elsewhere here:
|
|
76
|
+
* `rename` never resolves the final path component, so a symlink swapped in
|
|
77
|
+
* after {@link ensureLockfileIsNotSymlink} cannot redirect the write.
|
|
78
|
+
*/
|
|
79
|
+
async function writeWantedLockfileAtomic(lockfilePath, content) {
|
|
80
|
+
await ensureLockfileIsNotSymlink(lockfilePath);
|
|
81
|
+
const targetStat = await fs.lstat(lockfilePath).catch((error) => {
|
|
82
|
+
if (error.code === 'ENOENT')
|
|
83
|
+
return undefined;
|
|
84
|
+
throw error;
|
|
85
|
+
});
|
|
86
|
+
const tempPath = path.join(path.dirname(lockfilePath), `.${path.basename(lockfilePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
87
|
+
let tempFile;
|
|
88
|
+
try {
|
|
89
|
+
tempFile = await fs.open(tempPath, 'wx', targetStat?.mode);
|
|
90
|
+
await tempFile.writeFile(content);
|
|
91
|
+
if (targetStat != null) {
|
|
92
|
+
// An install running as root, in a container over a bind-mounted repo,
|
|
93
|
+
// would otherwise leave the owner a lockfile they cannot write.
|
|
94
|
+
await tempFile.chown(targetStat.uid, targetStat.gid).catch(ignoreUnprivilegedChown);
|
|
95
|
+
await tempFile.chmod(targetStat.mode);
|
|
96
|
+
}
|
|
97
|
+
await tempFile.sync();
|
|
98
|
+
await tempFile.close();
|
|
99
|
+
tempFile = undefined;
|
|
100
|
+
// Check again at publication time. rename() replaces the final path entry
|
|
101
|
+
// itself and never resolves it, so a swap after this check cannot redirect
|
|
102
|
+
// the write through a symlink.
|
|
103
|
+
await ensureLockfileIsNotSymlink(lockfilePath);
|
|
104
|
+
await fs.rename(tempPath, lockfilePath);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await tempFile?.close().catch(() => { });
|
|
108
|
+
await fs.rm(tempPath, { force: true }).catch(() => { });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* `chown` is refused for an unprivileged process that doesn't own the target,
|
|
113
|
+
* and unimplemented on some filesystems. Neither is a reason to fail the write.
|
|
114
|
+
*/
|
|
115
|
+
function ignoreUnprivilegedChown(error) {
|
|
116
|
+
const tolerated = error.code === 'ENOSYS' ||
|
|
117
|
+
((process.getuid == null || process.getuid() !== 0) && (error.code === 'EINVAL' || error.code === 'EPERM'));
|
|
118
|
+
if (!tolerated)
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
function stripUndefinedDeep(value) {
|
|
122
|
+
if (value === null || typeof value !== 'object')
|
|
123
|
+
return value;
|
|
124
|
+
if (Array.isArray(value))
|
|
125
|
+
return value.map(stripUndefinedDeep);
|
|
126
|
+
const out = {};
|
|
127
|
+
for (const [k, v] of Object.entries(value)) {
|
|
128
|
+
if (v === undefined)
|
|
129
|
+
continue;
|
|
130
|
+
out[k] = stripUndefinedDeep(v);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
export function writeLockfileFile(lockfilePath, wantedLockfile) {
|
|
135
|
+
const yamlDoc = yamlStringify(wantedLockfile);
|
|
136
|
+
return writeFileAtomic(lockfilePath, yamlDoc);
|
|
137
|
+
}
|
|
138
|
+
function yamlStringify(lockfile) {
|
|
139
|
+
const sortedLockfile = sortLockfileKeys(lockfile);
|
|
140
|
+
return lockfileYamlDump(sortedLockfile);
|
|
141
|
+
}
|
|
142
|
+
export function isEmptyLockfile(lockfile) {
|
|
143
|
+
return Object.values(lockfile.importers).every((importer) => isEmpty(importer.specifiers ?? {}) && isEmpty(importer.dependencies ?? {}));
|
|
144
|
+
}
|
|
145
|
+
export async function writeLockfiles(opts) {
|
|
146
|
+
const wantedLockfileName = opts.wantedLockfileName ?? await getWantedLockfileName(opts);
|
|
147
|
+
const wantedLockfilePath = path.join(opts.wantedLockfileDir, wantedLockfileName);
|
|
148
|
+
const currentLockfilePath = path.join(opts.currentLockfileDir, 'lock.yaml');
|
|
149
|
+
const wantedLockfileToStringify = convertToLockfileFile(opts.wantedLockfile);
|
|
150
|
+
const yamlDoc = yamlStringify(wantedLockfileToStringify);
|
|
151
|
+
// in most cases the `pnpm-lock.yaml` and `node_modules/.pnpm-lock.yaml` are equal
|
|
152
|
+
// in those cases the YAML document can be stringified only once for both files
|
|
153
|
+
// which is more efficient
|
|
154
|
+
if (opts.wantedLockfile === opts.currentLockfile) {
|
|
155
|
+
await Promise.all([
|
|
156
|
+
writeLockfileDoc(wantedLockfilePath, wantedLockfileName, yamlDoc),
|
|
157
|
+
(async () => {
|
|
158
|
+
if (isEmptyLockfile(opts.wantedLockfile)) {
|
|
159
|
+
await rimraf(currentLockfilePath);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
await fs.mkdir(path.dirname(currentLockfilePath), { recursive: true });
|
|
163
|
+
// Current lockfile (node_modules/.pnpm/lock.yaml) does not include the env document
|
|
164
|
+
await writeFileAtomic(currentLockfilePath, yamlDoc);
|
|
165
|
+
}
|
|
166
|
+
})(),
|
|
167
|
+
]);
|
|
168
|
+
// Both files share the same source object; strip once and reuse.
|
|
169
|
+
const normalized = convertToLockfileObject(stripUndefinedDeep(wantedLockfileToStringify));
|
|
170
|
+
return {
|
|
171
|
+
wantedLockfile: normalized,
|
|
172
|
+
currentLockfile: isEmptyLockfile(opts.wantedLockfile) ? undefined : normalized,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
logger.debug({
|
|
176
|
+
message: `\`${WANTED_LOCKFILE}\` differs from \`${path.relative(opts.wantedLockfileDir, currentLockfilePath)}\``,
|
|
177
|
+
prefix: opts.wantedLockfileDir,
|
|
178
|
+
});
|
|
179
|
+
const currentLockfileToStringify = convertToLockfileFile(opts.currentLockfile);
|
|
180
|
+
const currentYamlDoc = yamlStringify(currentLockfileToStringify);
|
|
181
|
+
// Filtered-current callers (deps-restorer) can pass an empty
|
|
182
|
+
// current against a non-empty wanted; key off the current.
|
|
183
|
+
const currentIsEmpty = isEmptyLockfile(opts.currentLockfile);
|
|
184
|
+
await Promise.all([
|
|
185
|
+
writeLockfileDoc(wantedLockfilePath, wantedLockfileName, yamlDoc),
|
|
186
|
+
(async () => {
|
|
187
|
+
if (currentIsEmpty) {
|
|
188
|
+
await rimraf(currentLockfilePath);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
await fs.mkdir(path.dirname(currentLockfilePath), { recursive: true });
|
|
192
|
+
await writeFileAtomic(currentLockfilePath, currentYamlDoc);
|
|
193
|
+
}
|
|
194
|
+
})(),
|
|
195
|
+
]);
|
|
196
|
+
return {
|
|
197
|
+
wantedLockfile: convertToLockfileObject(stripUndefinedDeep(wantedLockfileToStringify)),
|
|
198
|
+
currentLockfile: currentIsEmpty
|
|
199
|
+
? undefined
|
|
200
|
+
: convertToLockfileObject(stripUndefinedDeep(currentLockfileToStringify)),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=write.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const YAML_DOCUMENT_SEPARATOR = "\n---\n";
|
|
2
|
+
export declare const YAML_DOCUMENT_START = "---\n";
|
|
3
|
+
/**
|
|
4
|
+
* Reads the first YAML document from a multi-document YAML file using streaming.
|
|
5
|
+
* The file must start with "---\n" to indicate it contains an env lockfile document.
|
|
6
|
+
* Stops reading as soon as the second document separator is found.
|
|
7
|
+
* Returns null if the file doesn't exist or doesn't start with "---\n".
|
|
8
|
+
*/
|
|
9
|
+
export declare function streamReadFirstYamlDocument(filePath: string, readBufferSize?: number): Promise<string | null>;
|
|
10
|
+
export declare function readLockfileToString(filePath: string): Promise<string | null>;
|
|
11
|
+
export declare function readLockfileToStringNoFollow(filePath: string): Promise<string | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Refuses a symlinked lockfile before a write, which would land on the link's
|
|
14
|
+
* target — any file the user can write. Reads may follow it: sandboxes stage
|
|
15
|
+
* `pnpm-lock.yaml` as a symlink, and lockfile content is untrusted either way
|
|
16
|
+
* (https://github.com/pnpm/pnpm/issues/13073).
|
|
17
|
+
*/
|
|
18
|
+
export declare function ensureLockfileIsNotSymlink(filePath: string): Promise<void>;
|
|
19
|
+
/** The in-memory counterpart of {@link streamReadFirstYamlDocument}. */
|
|
20
|
+
export declare function extractEnvDocument(content: string): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Extracts the main lockfile content (second YAML document) from a combined string.
|
|
23
|
+
* If the file starts with "---\n", returns the content after the separator.
|
|
24
|
+
* If there is no separator, returns empty string (file is env-only).
|
|
25
|
+
* Otherwise returns the entire content (no env document present).
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractMainDocument(content: string): string;
|