upstreamfix 0.1.2 → 0.1.3
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/README.md +18 -1
- package/dist/cli.js +109 -29
- package/dist/core/diagnostic-diff.js +12 -9
- package/dist/core/diff-api.js +211 -0
- package/dist/core/docker-mount.js +6 -0
- package/dist/core/impact.js +72 -0
- package/dist/core/manifest.js +4 -0
- package/dist/core/node-range.js +19 -0
- package/dist/core/probe.js +155 -0
- package/dist/core/project-discovery.js +10 -0
- package/dist/core/report.js +70 -10
- package/dist/core/runner.js +136 -19
- package/dist/core/verify.js +110 -78
- package/dist/core/watch.js +81 -0
- package/dist/core/write-branch.js +45 -27
- package/dist/migrations/firebase-admin-v14/index.js +122 -10
- package/dist/migrations/index.js +3 -7
- package/dist/migrations/stripe-shared.js +75 -9
- package/dist/migrations/stripe-v23/index.js +2 -1
- package/package.json +1 -1
package/dist/core/runner.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { execFile as execFileCallback } from 'node:child_process';
|
|
3
3
|
import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { registerTemporaryCleanup } from './process-cleanup.js';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { tmpdir } from 'node:os';
|
|
7
8
|
import { structuredPatch } from 'diff';
|
|
8
9
|
import { Project } from 'ts-morph';
|
|
10
|
+
import { IGNORED_PROJECT_DIRECTORIES, isSupportedSourceMajor } from './project-discovery.js';
|
|
11
|
+
import { parseManifest } from './manifest.js';
|
|
9
12
|
const execFile = promisify(execFileCallback);
|
|
10
13
|
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
|
|
11
|
-
const
|
|
14
|
+
const MAX_DIFF_INPUT_BYTES = 1024 * 1024;
|
|
15
|
+
const MAX_DIFF_OUTPUT_CHARS = 64 * 1024;
|
|
16
|
+
const MAX_REPORT_DIFF_CHARS = 256 * 1024;
|
|
17
|
+
const LOCKFILE_NAMES = new Set(['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock']);
|
|
12
18
|
export function loadProject(cwd, packageName = 'firebase-admin') {
|
|
13
19
|
const project = new Project({
|
|
14
20
|
skipAddingFilesFromTsConfig: true,
|
|
@@ -27,7 +33,7 @@ function loadScopedProject(root, migration) {
|
|
|
27
33
|
let parsed;
|
|
28
34
|
let invalid = false;
|
|
29
35
|
try {
|
|
30
|
-
parsed =
|
|
36
|
+
parsed = parseManifest(readFileSync(file, 'utf8'));
|
|
31
37
|
}
|
|
32
38
|
catch {
|
|
33
39
|
invalid = true;
|
|
@@ -82,7 +88,7 @@ function loadScopedProject(root, migration) {
|
|
|
82
88
|
});
|
|
83
89
|
continue;
|
|
84
90
|
}
|
|
85
|
-
if (!
|
|
91
|
+
if (!isSupportedSourceMajor(declaration.range, migration.fromMajor)) {
|
|
86
92
|
manual.push({
|
|
87
93
|
file: sourcePath,
|
|
88
94
|
line: 1,
|
|
@@ -96,9 +102,6 @@ function loadScopedProject(root, migration) {
|
|
|
96
102
|
}
|
|
97
103
|
return { project, manual, manifestBySource };
|
|
98
104
|
}
|
|
99
|
-
function isSourceMajorRange(range, major) {
|
|
100
|
-
return new RegExp(`^\\s*[~^]?${major}(?:\\.(?:\\d+|x|\\*)){0,2}\\s*$`, 'i').test(range);
|
|
101
|
-
}
|
|
102
105
|
function isRegistryVersionSpec(spec) {
|
|
103
106
|
// A deliberately narrow subset: registry dist-tags or ordinary semver ranges.
|
|
104
107
|
// Anything that can name a path, URL, Git host, workspace, alias, or patch is manual.
|
|
@@ -138,6 +141,9 @@ export function lockfileCommandEnvironment(home) {
|
|
|
138
141
|
NPM_CONFIG_FUND: 'false',
|
|
139
142
|
NPM_CONFIG_IGNORE_SCRIPTS: 'true',
|
|
140
143
|
COREPACK_ENABLE_PROJECT_SPEC: '0',
|
|
144
|
+
COREPACK_ENABLE_AUTO_PIN: '0',
|
|
145
|
+
COREPACK_ENABLE_UNSAFE_CUSTOM_URLS: '0',
|
|
146
|
+
COREPACK_HOME: path.join(home, 'corepack'),
|
|
141
147
|
GIT_CONFIG_NOSYSTEM: '1',
|
|
142
148
|
GIT_TERMINAL_PROMPT: '0',
|
|
143
149
|
};
|
|
@@ -148,7 +154,7 @@ function findSourceFiles(root, packageName) {
|
|
|
148
154
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
149
155
|
const absolutePath = path.join(directory, entry.name);
|
|
150
156
|
if (entry.isDirectory()) {
|
|
151
|
-
if (!
|
|
157
|
+
if (!IGNORED_PROJECT_DIRECTORIES.has(entry.name))
|
|
152
158
|
visit(absolutePath);
|
|
153
159
|
continue;
|
|
154
160
|
}
|
|
@@ -172,7 +178,7 @@ export async function runMigration(migration, options) {
|
|
|
172
178
|
}
|
|
173
179
|
const scoped = loadScopedProject(options.cwd, migration);
|
|
174
180
|
const project = scoped.project;
|
|
175
|
-
const originalSources = new Map(project.getSourceFiles().map((file) => [file.getFilePath(), file.
|
|
181
|
+
const originalSources = new Map(await Promise.all(project.getSourceFiles().map(async (file) => [file.getFilePath(), await readFile(file.getFilePath(), 'utf8')])));
|
|
176
182
|
const result = migration.transform(project);
|
|
177
183
|
result.manual.unshift(...scoped.manual);
|
|
178
184
|
const compatibilityNotes = migration.inspectManifest
|
|
@@ -182,7 +188,7 @@ export async function runMigration(migration, options) {
|
|
|
182
188
|
throw new Error('This migration has manual review items; no files were written. Resolve them and preview again.');
|
|
183
189
|
}
|
|
184
190
|
const manifestsWithChangedCode = new Set(result.filesChanged.map((sourcePath) => scoped.manifestBySource.get(sourcePath)).filter((file) => Boolean(file)));
|
|
185
|
-
const manifest = result.manual.length === 0 && result.changes.length > 0
|
|
191
|
+
const manifest = !options.analysisOnly && result.manual.length === 0 && result.changes.length > 0
|
|
186
192
|
? await migrateManifests(migration, options, manifestsWithChangedCode)
|
|
187
193
|
: {
|
|
188
194
|
notes: [result.manual.length > 0
|
|
@@ -194,6 +200,11 @@ export async function runMigration(migration, options) {
|
|
|
194
200
|
if (!options.dryRun && manifest.manual.length > 0) {
|
|
195
201
|
throw new Error('A dependency manifest needs manual review; no files were written. Resolve it and preview again.');
|
|
196
202
|
}
|
|
203
|
+
for (const update of result.fileUpdates ?? []) {
|
|
204
|
+
if (originalSources.get(update.file)?.startsWith('\uFEFF') && !update.contents.startsWith('\uFEFF')) {
|
|
205
|
+
update.contents = `\uFEFF${update.contents}`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
197
208
|
const sourceUpdates = new Map((result.fileUpdates ?? []).map((update) => [update.file, update.contents]));
|
|
198
209
|
const proposedFiles = [];
|
|
199
210
|
const proposedDiff = result.filesChanged.flatMap((filePath) => {
|
|
@@ -212,6 +223,15 @@ export async function runMigration(migration, options) {
|
|
|
212
223
|
if (diff)
|
|
213
224
|
proposedDiff.push(diff);
|
|
214
225
|
}
|
|
226
|
+
let reportedCharacters = 0;
|
|
227
|
+
for (let index = 0; index < proposedDiff.length; index++) {
|
|
228
|
+
const remaining = MAX_REPORT_DIFF_CHARS - reportedCharacters;
|
|
229
|
+
if (remaining < proposedDiff[index].length) {
|
|
230
|
+
proposedDiff.splice(index, proposedDiff.length - index, `[Further diffs withheld: the report limit is ${MAX_REPORT_DIFF_CHARS} characters.]`);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
reportedCharacters += proposedDiff[index].length;
|
|
234
|
+
}
|
|
215
235
|
return {
|
|
216
236
|
...result,
|
|
217
237
|
filesChanged: [
|
|
@@ -232,7 +252,7 @@ async function inspectManifests(migration, cwd) {
|
|
|
232
252
|
if (!raw.includes(JSON.stringify(migration.packageName)))
|
|
233
253
|
continue;
|
|
234
254
|
try {
|
|
235
|
-
const manifest =
|
|
255
|
+
const manifest = parseManifest(raw);
|
|
236
256
|
notes.push(...(migration.inspectManifest?.(manifest) ?? []));
|
|
237
257
|
}
|
|
238
258
|
catch {
|
|
@@ -258,7 +278,7 @@ async function migrateManifests(migration, options, manifestsWithChangedCode) {
|
|
|
258
278
|
}
|
|
259
279
|
let manifest;
|
|
260
280
|
try {
|
|
261
|
-
manifest =
|
|
281
|
+
manifest = parseManifest(raw);
|
|
262
282
|
}
|
|
263
283
|
catch {
|
|
264
284
|
manual.push({
|
|
@@ -323,7 +343,9 @@ async function migrateManifests(migration, options, manifestsWithChangedCode) {
|
|
|
323
343
|
: existingLocks.length === 1 && path.basename(existingLocks[0]) === 'pnpm-lock.yaml'
|
|
324
344
|
? await regeneratePnpmLock(manifestPath, path.resolve(options.cwd, existingLocks[0]), updated, options.cwd)
|
|
325
345
|
: existingLocks.length === 1 && path.basename(existingLocks[0]) === 'yarn.lock'
|
|
326
|
-
?
|
|
346
|
+
? options.allowYarnLockInstall
|
|
347
|
+
? await regenerateYarnLock(manifestPath, path.resolve(options.cwd, existingLocks[0]), updated, options.cwd)
|
|
348
|
+
: { reason: 'Yarn Classic lockfile regeneration requires --allow-yarn-lock-install because it runs a full dependency installation (estimated 50–400 MB of downloads without a warm cache).' }
|
|
327
349
|
: { reason: `This package uses unsupported lockfile(s): ${existingLocks.join(', ')}.` };
|
|
328
350
|
if (!lockResult.contents) {
|
|
329
351
|
notes.push(...fileNotes.filter((note) => !note.includes(' → ')));
|
|
@@ -375,7 +397,7 @@ async function regeneratePackageLock(manifestPath, lockPath, updatedManifest, pr
|
|
|
375
397
|
let before;
|
|
376
398
|
let lock;
|
|
377
399
|
try {
|
|
378
|
-
manifest =
|
|
400
|
+
manifest = parseManifest(updatedManifest);
|
|
379
401
|
before = await readFile(lockPath, 'utf8');
|
|
380
402
|
lock = JSON.parse(before);
|
|
381
403
|
}
|
|
@@ -401,11 +423,14 @@ async function regeneratePackageLock(manifestPath, lockPath, updatedManifest, pr
|
|
|
401
423
|
configDirectory = path.dirname(configDirectory);
|
|
402
424
|
}
|
|
403
425
|
let temporaryDirectory;
|
|
426
|
+
let unregisterCleanup;
|
|
404
427
|
try {
|
|
405
428
|
temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'upstreamfix-npm-lock-'));
|
|
429
|
+
unregisterCleanup = registerTemporaryCleanup(() => rm(temporaryDirectory, { recursive: true, force: true }));
|
|
406
430
|
await writeFile(path.join(temporaryDirectory, 'package.json'), updatedManifest, 'utf8');
|
|
407
431
|
await writeFile(path.join(temporaryDirectory, 'package-lock.json'), before, 'utf8');
|
|
408
432
|
const args = ['install', '--package-lock-only', '--lockfile-version=3', '--ignore-scripts', '--no-audit', '--no-fund'];
|
|
433
|
+
// npm_execpath may be pnpm or Yarn when launched through dlx; resolve npm from PATH instead.
|
|
409
434
|
await execFile('npm', args, { cwd: temporaryDirectory, env: lockfileCommandEnvironment(temporaryDirectory), timeout: 120_000, maxBuffer: 4 * 1024 * 1024 });
|
|
410
435
|
const contents = await readFile(path.join(temporaryDirectory, 'package-lock.json'), 'utf8');
|
|
411
436
|
return { before, contents };
|
|
@@ -414,6 +439,7 @@ async function regeneratePackageLock(manifestPath, lockPath, updatedManifest, pr
|
|
|
414
439
|
return { reason: 'npm could not regenerate package-lock.json in an isolated temporary directory.' };
|
|
415
440
|
}
|
|
416
441
|
finally {
|
|
442
|
+
unregisterCleanup?.();
|
|
417
443
|
if (temporaryDirectory)
|
|
418
444
|
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
419
445
|
}
|
|
@@ -429,7 +455,7 @@ async function regeneratePnpmLock(manifestPath, lockPath, updatedManifest, proje
|
|
|
429
455
|
let manifest;
|
|
430
456
|
let before;
|
|
431
457
|
try {
|
|
432
|
-
manifest =
|
|
458
|
+
manifest = parseManifest(updatedManifest);
|
|
433
459
|
before = await readFile(lockPath, 'utf8');
|
|
434
460
|
}
|
|
435
461
|
catch {
|
|
@@ -459,8 +485,10 @@ async function regeneratePnpmLock(manifestPath, lockPath, updatedManifest, proje
|
|
|
459
485
|
return { reason: 'The packageManager field does not pin an exact pnpm version, so lockfile regeneration cannot be version-checked safely.' };
|
|
460
486
|
}
|
|
461
487
|
let temporaryDirectory;
|
|
488
|
+
let unregisterCleanup;
|
|
462
489
|
try {
|
|
463
490
|
const probeHome = await mkdtemp(path.join(tmpdir(), 'upstreamfix-pnpm-home-'));
|
|
491
|
+
const unregisterProbeHome = registerTemporaryCleanup(() => rm(probeHome, { recursive: true, force: true }));
|
|
464
492
|
let versionOutput;
|
|
465
493
|
try {
|
|
466
494
|
({ stdout: versionOutput } = await execFile('pnpm', ['--version'], {
|
|
@@ -471,6 +499,7 @@ async function regeneratePnpmLock(manifestPath, lockPath, updatedManifest, proje
|
|
|
471
499
|
}));
|
|
472
500
|
}
|
|
473
501
|
finally {
|
|
502
|
+
unregisterProbeHome();
|
|
474
503
|
await rm(probeHome, { recursive: true, force: true });
|
|
475
504
|
}
|
|
476
505
|
const installedVersion = versionOutput.trim();
|
|
@@ -484,6 +513,7 @@ async function regeneratePnpmLock(manifestPath, lockPath, updatedManifest, proje
|
|
|
484
513
|
}
|
|
485
514
|
}
|
|
486
515
|
temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'upstreamfix-pnpm-lock-'));
|
|
516
|
+
unregisterCleanup = registerTemporaryCleanup(() => rm(temporaryDirectory, { recursive: true, force: true }));
|
|
487
517
|
// pnpm reads packageManager and may download a different CLI automatically. The exact
|
|
488
518
|
// version was checked above, so omit this metadata from the isolated temporary manifest.
|
|
489
519
|
delete manifest.packageManager;
|
|
@@ -506,6 +536,7 @@ async function regeneratePnpmLock(manifestPath, lockPath, updatedManifest, proje
|
|
|
506
536
|
return { reason: 'pnpm could not regenerate pnpm-lock.yaml in an isolated temporary directory; ensure the pinned pnpm version is installed and registry access is available.' };
|
|
507
537
|
}
|
|
508
538
|
finally {
|
|
539
|
+
unregisterCleanup?.();
|
|
509
540
|
if (temporaryDirectory)
|
|
510
541
|
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
511
542
|
}
|
|
@@ -521,7 +552,7 @@ async function regenerateYarnLock(manifestPath, lockPath, updatedManifest, proje
|
|
|
521
552
|
let manifest;
|
|
522
553
|
let before;
|
|
523
554
|
try {
|
|
524
|
-
manifest =
|
|
555
|
+
manifest = parseManifest(updatedManifest);
|
|
525
556
|
before = await readFile(lockPath, 'utf8');
|
|
526
557
|
}
|
|
527
558
|
catch {
|
|
@@ -554,8 +585,10 @@ async function regenerateYarnLock(manifestPath, lockPath, updatedManifest, proje
|
|
|
554
585
|
return { reason: 'Only an exact Yarn Classic 1.x packageManager pin is supported for automatic lockfile regeneration.' };
|
|
555
586
|
}
|
|
556
587
|
let temporaryDirectory;
|
|
588
|
+
let unregisterCleanup;
|
|
557
589
|
try {
|
|
558
590
|
const probeHome = await mkdtemp(path.join(tmpdir(), 'upstreamfix-yarn-home-'));
|
|
591
|
+
const unregisterProbeHome = registerTemporaryCleanup(() => rm(probeHome, { recursive: true, force: true }));
|
|
559
592
|
let versionOutput;
|
|
560
593
|
try {
|
|
561
594
|
({ stdout: versionOutput } = await execFile('yarn', ['--version'], {
|
|
@@ -566,6 +599,7 @@ async function regenerateYarnLock(manifestPath, lockPath, updatedManifest, proje
|
|
|
566
599
|
}));
|
|
567
600
|
}
|
|
568
601
|
finally {
|
|
602
|
+
unregisterProbeHome();
|
|
569
603
|
await rm(probeHome, { recursive: true, force: true });
|
|
570
604
|
}
|
|
571
605
|
const installedVersion = versionOutput.trim();
|
|
@@ -579,6 +613,7 @@ async function regenerateYarnLock(manifestPath, lockPath, updatedManifest, proje
|
|
|
579
613
|
}
|
|
580
614
|
}
|
|
581
615
|
temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'upstreamfix-yarn-lock-'));
|
|
616
|
+
unregisterCleanup = registerTemporaryCleanup(() => rm(temporaryDirectory, { recursive: true, force: true }));
|
|
582
617
|
delete manifest.packageManager;
|
|
583
618
|
await writeFile(path.join(temporaryDirectory, 'package.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
584
619
|
await writeFile(path.join(temporaryDirectory, 'yarn.lock'), before, 'utf8');
|
|
@@ -598,6 +633,7 @@ async function regenerateYarnLock(manifestPath, lockPath, updatedManifest, proje
|
|
|
598
633
|
return { reason: 'Yarn Classic could not regenerate yarn.lock in an isolated temporary directory; ensure the pinned Yarn 1.x version is installed and registry access is available.' };
|
|
599
634
|
}
|
|
600
635
|
finally {
|
|
636
|
+
unregisterCleanup?.();
|
|
601
637
|
if (temporaryDirectory)
|
|
602
638
|
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
603
639
|
}
|
|
@@ -640,7 +676,7 @@ function findPackageManifests(root) {
|
|
|
640
676
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
641
677
|
const absolutePath = path.join(directory, entry.name);
|
|
642
678
|
if (entry.isDirectory()) {
|
|
643
|
-
if (!
|
|
679
|
+
if (!IGNORED_PROJECT_DIRECTORIES.has(entry.name))
|
|
644
680
|
visit(absolutePath);
|
|
645
681
|
}
|
|
646
682
|
else if (entry.isFile() && entry.name === 'package.json') {
|
|
@@ -656,7 +692,7 @@ function addNodeEngineToManifest(raw) {
|
|
|
656
692
|
if (raw[rootStart] !== '{')
|
|
657
693
|
return undefined;
|
|
658
694
|
const engineMember = findJsonObjectMember(raw, rootStart, 'engines');
|
|
659
|
-
if (!engineMember && Object.hasOwn(
|
|
695
|
+
if (!engineMember && Object.hasOwn(parseManifest(raw), 'engines'))
|
|
660
696
|
return undefined;
|
|
661
697
|
const objectStart = engineMember?.valueStart ?? rootStart;
|
|
662
698
|
if (raw[objectStart] !== '{')
|
|
@@ -676,7 +712,7 @@ function addNodeEngineToManifest(raw) {
|
|
|
676
712
|
: `${hasMembers ? ', ' : ''}${engineMember ? '"node": ">=22"' : '"engines": { "node": ">=22" }'}`;
|
|
677
713
|
const updated = raw.slice(0, insertAt) + addition + raw.slice(insertAt);
|
|
678
714
|
try {
|
|
679
|
-
const parsed =
|
|
715
|
+
const parsed = parseManifest(updated);
|
|
680
716
|
return parsed.engines?.node === '>=22' ? updated : undefined;
|
|
681
717
|
}
|
|
682
718
|
catch {
|
|
@@ -816,11 +852,92 @@ function jsonValueEnd(text, start) {
|
|
|
816
852
|
export function unifiedDiff(file, before, after) {
|
|
817
853
|
if (before === after)
|
|
818
854
|
return '';
|
|
855
|
+
if (LOCKFILE_NAMES.has(path.basename(file)))
|
|
856
|
+
return summarizeLockfile(file, before, after);
|
|
857
|
+
if (Buffer.byteLength(before) + Buffer.byteLength(after) > MAX_DIFF_INPUT_BYTES) {
|
|
858
|
+
return `${file}: diff withheld because the source exceeds ${MAX_DIFF_INPUT_BYTES} bytes.`;
|
|
859
|
+
}
|
|
819
860
|
const patch = structuredPatch(`a/${file}`, `b/${file}`, before.replace(/\r\n/g, '\n'), after.replace(/\r\n/g, '\n'), '', '', { context: 3 });
|
|
820
861
|
const output = [`--- a/${file}`, `+++ b/${file}`];
|
|
821
862
|
for (const hunk of patch.hunks) {
|
|
822
863
|
output.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
|
|
823
864
|
output.push(...hunk.lines);
|
|
824
865
|
}
|
|
825
|
-
|
|
866
|
+
const rendered = output.join('\n');
|
|
867
|
+
return rendered.length <= MAX_DIFF_OUTPUT_CHARS
|
|
868
|
+
? rendered
|
|
869
|
+
: `${file}: diff withheld because it exceeds ${MAX_DIFF_OUTPUT_CHARS} characters.`;
|
|
870
|
+
}
|
|
871
|
+
export function summarizeLockfile(file, before, after) {
|
|
872
|
+
const previous = lockfilePackages(file, before);
|
|
873
|
+
const next = lockfilePackages(file, after);
|
|
874
|
+
if (!previous || !next)
|
|
875
|
+
return `${file}: lockfile changed; package counts unavailable (file too large or unrecognized format). Full diff withheld.`;
|
|
876
|
+
let added = 0;
|
|
877
|
+
let removed = 0;
|
|
878
|
+
let changed = 0;
|
|
879
|
+
for (const [name, version] of next) {
|
|
880
|
+
if (!previous.has(name))
|
|
881
|
+
added++;
|
|
882
|
+
else if (previous.get(name) !== version)
|
|
883
|
+
changed++;
|
|
884
|
+
}
|
|
885
|
+
for (const name of previous.keys())
|
|
886
|
+
if (!next.has(name))
|
|
887
|
+
removed++;
|
|
888
|
+
return `${file}: lockfile summary — added: ${added}, removed: ${removed}, changed: ${changed}. Full diff withheld.`;
|
|
889
|
+
}
|
|
890
|
+
function lockfilePackages(file, contents) {
|
|
891
|
+
if (Buffer.byteLength(contents) > 30 * 1024 * 1024)
|
|
892
|
+
return undefined;
|
|
893
|
+
if (file.endsWith('.json')) {
|
|
894
|
+
try {
|
|
895
|
+
const parsed = JSON.parse(contents);
|
|
896
|
+
if (!parsed.packages || typeof parsed.packages !== 'object')
|
|
897
|
+
return undefined;
|
|
898
|
+
return new Map(Object.entries(parsed.packages)
|
|
899
|
+
.filter(([name, value]) => name !== '' && value && typeof value.version === 'string')
|
|
900
|
+
.map(([name, value]) => [name, value.version]));
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
return undefined;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const packages = new Map();
|
|
907
|
+
let section = '';
|
|
908
|
+
let name = '';
|
|
909
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
910
|
+
if (file.endsWith('pnpm-lock.yaml')) {
|
|
911
|
+
if (/^(packages|snapshots):\s*$/.test(line)) {
|
|
912
|
+
section = line.slice(0, -1);
|
|
913
|
+
name = '';
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (/^[^\s#]/.test(line)) {
|
|
917
|
+
section = '';
|
|
918
|
+
name = '';
|
|
919
|
+
}
|
|
920
|
+
const entry = section === 'packages' ? /^ (['"]?)(.+?)\1:\s*$/.exec(line) : null;
|
|
921
|
+
if (entry) {
|
|
922
|
+
const rawName = entry[2].replace(/^\//, '').replace(/\(.*$/, '');
|
|
923
|
+
const separator = rawName.lastIndexOf('@');
|
|
924
|
+
name = separator > 0 ? rawName.slice(0, separator) : rawName;
|
|
925
|
+
packages.set(name, separator > 0 ? rawName.slice(separator + 1).split('(')[0] : rawName);
|
|
926
|
+
}
|
|
927
|
+
const version = /^ version:\s*['"]?([^\s'"]+)/.exec(line);
|
|
928
|
+
if (name && version)
|
|
929
|
+
packages.set(name, version[1]);
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
if (/^[^\s#].*:\s*$/.test(line)) {
|
|
933
|
+
const selector = line.slice(0, -1).split(',')[0].trim().replace(/^['"]|['"]$/g, '');
|
|
934
|
+
const separator = selector.lastIndexOf('@');
|
|
935
|
+
name = separator > 0 ? selector.slice(0, separator) : selector;
|
|
936
|
+
}
|
|
937
|
+
const version = /^ version\s+['"]([^'"]+)['"]/.exec(line);
|
|
938
|
+
if (name && version)
|
|
939
|
+
packages.set(name, version[1]);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return packages.size > 0 ? packages : undefined;
|
|
826
943
|
}
|