@dependably/npm-check 1.7.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 +201 -0
- package/README.md +71 -0
- package/bin/cli.js +1577 -0
- package/package.json +103 -0
- package/src/audit-config.js +331 -0
- package/src/audit.js +805 -0
- package/src/backup.js +235 -0
- package/src/checker.js +771 -0
- package/src/checksum-fixer.js +419 -0
- package/src/deprecation.js +325 -0
- package/src/fixer.js +207 -0
- package/src/format-library.js +132 -0
- package/src/index.js +138 -0
- package/src/integrity.js +519 -0
- package/src/migrator.js +263 -0
- package/src/npmrc-validator.js +205 -0
- package/src/overrides.js +71 -0
- package/src/package-json-validator.js +356 -0
- package/src/parallel-processor.js +374 -0
- package/src/parser.js +133 -0
- package/src/performance.js +283 -0
- package/src/pinner.js +248 -0
- package/src/pnpm-format.js +229 -0
- package/src/pnpm-workspace-validator.js +121 -0
- package/src/progress-reporter.js +245 -0
- package/src/pruner.js +177 -0
- package/src/remediate.js +389 -0
- package/src/report.js +663 -0
- package/src/schema.js +76 -0
- package/src/streaming-parser.js +251 -0
- package/src/updater.js +251 -0
- package/src/usage-scanner.js +215 -0
- package/src/validator.js +282 -0
- package/src/vuln.js +618 -0
- package/src/workers/dedupe-worker.js +20 -0
- package/src/workers/hash-upgrade-worker.js +20 -0
- package/src/workers/migration-worker.js +21 -0
- package/src/workers/validation-worker.js +20 -0
package/src/backup.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// src/backup.js
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
|
|
6
|
+
export class BackupError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'BackupError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Derive the backup directory for a given source file.
|
|
15
|
+
* Backups live in a `.backups/` sibling directory next to the source file,
|
|
16
|
+
* so they follow the file regardless of the caller's cwd.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} absFilePath - Absolute path to the source file
|
|
19
|
+
* @returns {string} Absolute path to the backup directory
|
|
20
|
+
*/
|
|
21
|
+
function backupDirFor(absFilePath) {
|
|
22
|
+
return path.join(path.dirname(absFilePath), '.backups');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A short, stable hex hash of the absolute source path. Used as part of the
|
|
27
|
+
* backup filename so that two files with the same basename in different
|
|
28
|
+
* directories (e.g. `web/package-lock.json` and `api/package-lock.json`) get
|
|
29
|
+
* distinct backup names and never overwrite each other's history.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} absPath - Absolute path to hash
|
|
32
|
+
* @returns {string} 8-character lowercase hex string
|
|
33
|
+
*/
|
|
34
|
+
function pathHash(absPath) {
|
|
35
|
+
// Not security-sensitive — just an 8-char discriminator so same-basename files
|
|
36
|
+
// in different directories get distinct backup names. sha256 (over sha1) keeps
|
|
37
|
+
// static analysis happy about weak hashes at zero cost.
|
|
38
|
+
return crypto.createHash('sha256').update(absPath).digest('hex').slice(0, 8);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a backup directory if it doesn't exist.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} dir - Absolute path to the backup directory
|
|
45
|
+
* @throws {BackupError} If directory creation fails
|
|
46
|
+
*/
|
|
47
|
+
function ensureBackupsDir(dir) {
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(dir)) {
|
|
50
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
throw new BackupError(`Failed to create backups directory: ${e.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a timestamped backup of a file.
|
|
59
|
+
*
|
|
60
|
+
* The backup is placed in a `.backups/` directory next to the source file so
|
|
61
|
+
* that backups are always adjacent to what they protect and are not sensitive
|
|
62
|
+
* to the caller's cwd.
|
|
63
|
+
*
|
|
64
|
+
* Filename format: `<basename>.<pathHash8>.<isoTimestampMs>.bak`
|
|
65
|
+
*
|
|
66
|
+
* - `pathHash8` — 8-char hash of the absolute source path, preventing
|
|
67
|
+
* cross-file collisions when two files share the same basename.
|
|
68
|
+
* - Millisecond-precision ISO timestamp — prevents same-second overwrites.
|
|
69
|
+
* - `wx` open flag — refuses to overwrite an existing file; a monotonically
|
|
70
|
+
* increasing counter suffix (`.1`, `.2`, …) is appended on the rare
|
|
71
|
+
* EEXIST collision (e.g. two calls within the same millisecond in tests).
|
|
72
|
+
*
|
|
73
|
+
* @param {string} filePath - Path to the file to backup (absolute or relative)
|
|
74
|
+
* @returns {string} Absolute path to the created backup file
|
|
75
|
+
* @throws {BackupError} If the source file is missing or backup creation fails
|
|
76
|
+
*/
|
|
77
|
+
export function createBackup(filePath) {
|
|
78
|
+
const absPath = path.resolve(filePath);
|
|
79
|
+
if (!fs.existsSync(absPath)) {
|
|
80
|
+
throw new BackupError(`File not found: ${filePath}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const backupDir = backupDirFor(absPath);
|
|
84
|
+
try {
|
|
85
|
+
ensureBackupsDir(backupDir);
|
|
86
|
+
|
|
87
|
+
const basename = path.basename(absPath);
|
|
88
|
+
const hash = pathHash(absPath);
|
|
89
|
+
// ms-precision: YYYY-MM-DDTHH-mm-ss-mmmZ (all colons/dots replaced)
|
|
90
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
91
|
+
const baseBackupName = `${basename}.${hash}.${timestamp}.bak`;
|
|
92
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
93
|
+
|
|
94
|
+
// Open with 'wx' — errors on EEXIST rather than overwriting. On the rare
|
|
95
|
+
// same-millisecond collision append a counter and retry.
|
|
96
|
+
let backupPath = path.join(backupDir, baseBackupName);
|
|
97
|
+
let counter = 0;
|
|
98
|
+
for (;;) {
|
|
99
|
+
try {
|
|
100
|
+
fs.writeFileSync(backupPath, content, { encoding: 'utf8', flag: 'wx' });
|
|
101
|
+
break;
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (e.code === 'EEXIST') {
|
|
104
|
+
counter++;
|
|
105
|
+
backupPath = path.join(backupDir, `${baseBackupName}.${counter}`);
|
|
106
|
+
} else {
|
|
107
|
+
throw e;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return backupPath;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (e instanceof BackupError) throw e;
|
|
115
|
+
throw new BackupError(`Failed to create backup: ${e.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* List all backups for a given source file, ordered newest-first.
|
|
121
|
+
*
|
|
122
|
+
* Backups are scoped by both the basename and the path hash, so only backups
|
|
123
|
+
* for this exact file are returned — not backups for same-named files in other
|
|
124
|
+
* directories.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} filePath - Path to the source file (not a bare basename)
|
|
127
|
+
* @returns {Array<{name: string, path: string, timestamp: string, created: Date}>}
|
|
128
|
+
* @throws {BackupError} If backup listing fails
|
|
129
|
+
*/
|
|
130
|
+
export function listBackups(filePath) {
|
|
131
|
+
const absPath = path.resolve(filePath);
|
|
132
|
+
const backupDir = backupDirFor(absPath);
|
|
133
|
+
try {
|
|
134
|
+
ensureBackupsDir(backupDir);
|
|
135
|
+
|
|
136
|
+
if (!fs.existsSync(backupDir)) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const basename = path.basename(absPath);
|
|
141
|
+
const hash = pathHash(absPath);
|
|
142
|
+
// All backups for this exact source file share this prefix.
|
|
143
|
+
const prefix = `${basename}.${hash}.`;
|
|
144
|
+
|
|
145
|
+
const files = fs.readdirSync(backupDir);
|
|
146
|
+
const backups = files
|
|
147
|
+
.filter(f => f.startsWith(prefix) && f.includes('.bak'))
|
|
148
|
+
.map(f => {
|
|
149
|
+
const backupPath = path.join(backupDir, f);
|
|
150
|
+
const stats = fs.statSync(backupPath);
|
|
151
|
+
// Timestamp is everything between the prefix and ".bak"
|
|
152
|
+
let timestamp = 'unknown';
|
|
153
|
+
const withoutPrefix = f.slice(prefix.length);
|
|
154
|
+
const bakIdx = withoutPrefix.indexOf('.bak');
|
|
155
|
+
if (bakIdx !== -1) timestamp = withoutPrefix.slice(0, bakIdx);
|
|
156
|
+
return {
|
|
157
|
+
name: f,
|
|
158
|
+
path: backupPath,
|
|
159
|
+
timestamp,
|
|
160
|
+
created: stats.mtime
|
|
161
|
+
};
|
|
162
|
+
})
|
|
163
|
+
.sort((a, b) => b.created - a.created);
|
|
164
|
+
|
|
165
|
+
return backups;
|
|
166
|
+
} catch (e) {
|
|
167
|
+
if (e instanceof BackupError) throw e;
|
|
168
|
+
throw new BackupError(`Failed to list backups: ${e.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Restore a file from its most recent backup.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} filePath - Path to the file to restore
|
|
176
|
+
* @returns {boolean} True if restoration was successful
|
|
177
|
+
* @throws {BackupError} If restoration fails or no backups exist
|
|
178
|
+
*/
|
|
179
|
+
export function restoreFromLatestBackup(filePath) {
|
|
180
|
+
try {
|
|
181
|
+
const absPath = path.resolve(filePath);
|
|
182
|
+
const backups = listBackups(absPath);
|
|
183
|
+
|
|
184
|
+
if (backups.length === 0) {
|
|
185
|
+
throw new BackupError(`No backups found for ${filePath}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const latestBackup = backups[0];
|
|
189
|
+
const backupContent = fs.readFileSync(latestBackup.path, 'utf8');
|
|
190
|
+
fs.writeFileSync(absPath, backupContent, 'utf8');
|
|
191
|
+
|
|
192
|
+
console.log(`Restored ${path.basename(absPath)} from backup: ${latestBackup.name}`);
|
|
193
|
+
return true;
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (e instanceof BackupError) throw e;
|
|
196
|
+
throw new BackupError(`Failed to restore backup: ${e.message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Clean old backups for a source file, keeping only the most recent N.
|
|
202
|
+
*
|
|
203
|
+
* Cleanup is scoped to the specific source file (by absolute path) so that
|
|
204
|
+
* pruning one file's history never deletes backups for a same-named file in a
|
|
205
|
+
* different directory.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} filePath - Path to the source file
|
|
208
|
+
* @param {number} keepCount - Number of backups to keep (default: 5)
|
|
209
|
+
* @returns {number} Number of backups deleted
|
|
210
|
+
* @throws {BackupError} If cleanup fails
|
|
211
|
+
*/
|
|
212
|
+
export function cleanOldBackups(filePath, keepCount = 5) {
|
|
213
|
+
try {
|
|
214
|
+
const absPath = path.resolve(filePath);
|
|
215
|
+
const backups = listBackups(absPath);
|
|
216
|
+
if (backups.length <= keepCount) {
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const toDelete = backups.slice(keepCount);
|
|
221
|
+
let deleted = 0;
|
|
222
|
+
|
|
223
|
+
for (const backup of toDelete) {
|
|
224
|
+
fs.unlinkSync(backup.path);
|
|
225
|
+
deleted++;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return deleted;
|
|
229
|
+
} catch (e) {
|
|
230
|
+
if (e instanceof BackupError) throw e;
|
|
231
|
+
throw new BackupError(`Failed to clean old backups: ${e.message}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default { createBackup, listBackups, restoreFromLatestBackup, cleanOldBackups, BackupError };
|