mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +02:00
fix: write/repair sync npm lockfiles' version fields (#2567)
npm records the package version twice in its lockfiles — top-level `version` and, in lockfileVersion >= 2, `packages[""].version` (the entry describing the root package itself) — and `npm install` keeps both in step. gstack-version-bump write/repair updated VERSION + package.json but left the lockfile behind, so every /ship bump in an npm repo drifted one field per release until someone ran npm, dirtying the tree on the next `npm install` far from the cause. write and repair now mirror the version into package-lock.json AND npm-shrinkwrap.json (which shares the format and, when present, is what npm actually honors) as a pure JSON edit — no npm spawn, no dependency-tree churn, dependency entries untouched. Per the wave plan's version-tooling end-state spec (decision 11): synced ONLY when the file already exists, never created (gstack itself is bun-only). A failed manifest/lockfile write keeps the existing exit-3 half-write semantics so classify reports DRIFT_STALE_PKG on re-run instead of hiding the drift. Tests: 5 new cases in test/gstack-version-bump.test.ts — both lockfile version fields synced with deps untouched, repair heals a stale lockfile, lockfileVersion 1 (no packages map) doesn't crash, npm-shrinkwrap.json synced without inventing a package-lock.json, malformed lockfile exits 3 loudly (26 pass total in the file). Re-derived from PR #2568 by @ortonom under decision 11. Fixes #2567 Co-authored-by: ortonom <3261546+ortonom@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
ortonom
Claude Fable 5
parent
d7ab20ac06
commit
c33b371f25
@@ -62,7 +62,7 @@ describe('write (FRESH bump)', () => {
|
||||
fs.writeFileSync(path.join(dir, 'VERSION'), '1.0.0.0\n');
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'x', version: '1.0.0.0', scripts: { t: 'y' } }, null, 2) + '\n');
|
||||
const out = execFileSync('bun', [BIN, 'write', '--version', '1.1.0.0'], { cwd: dir }).toString();
|
||||
expect(JSON.parse(out)).toEqual({ wrote: '1.1.0.0', packageJson: true });
|
||||
expect(JSON.parse(out)).toEqual({ wrote: '1.1.0.0', packageJson: true, packageLock: false });
|
||||
expect(fs.readFileSync(path.join(dir, 'VERSION'), 'utf-8').trim()).toBe('1.1.0.0');
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
||||
expect(pkg.version).toBe('1.1.0.0');
|
||||
@@ -80,7 +80,7 @@ describe('write (FRESH bump)', () => {
|
||||
const d2 = fs.mkdtempSync(path.join(os.tmpdir(), 'vbump-noPkg-'));
|
||||
fs.writeFileSync(path.join(d2, 'VERSION'), '0.1.0.0\n');
|
||||
const out = execFileSync('bun', [BIN, 'write', '--version', '0.2.0.0'], { cwd: d2 }).toString();
|
||||
expect(JSON.parse(out)).toEqual({ wrote: '0.2.0.0', packageJson: false });
|
||||
expect(JSON.parse(out)).toEqual({ wrote: '0.2.0.0', packageJson: false, packageLock: false });
|
||||
expect(fs.readFileSync(path.join(d2, 'VERSION'), 'utf-8').trim()).toBe('0.2.0.0');
|
||||
fs.rmSync(d2, { recursive: true, force: true });
|
||||
});
|
||||
@@ -108,6 +108,78 @@ describe('repair (DRIFT_STALE_PKG)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('write/repair sync npm lockfiles (both version fields, #2567)', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vbump-lock-'));
|
||||
afterAll(() => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* noop */ } });
|
||||
|
||||
const lock = (v: string) => JSON.stringify({
|
||||
name: 'x', version: v, lockfileVersion: 3,
|
||||
packages: { '': { name: 'x', version: v }, 'node_modules/a': { version: '9.9.9' } },
|
||||
}, null, 2) + '\n';
|
||||
|
||||
test('write updates top-level version and packages[""].version, leaves deps alone', () => {
|
||||
fs.writeFileSync(path.join(dir, 'VERSION'), '1.0.0.0\n');
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'x', version: '1.0.0.0' }, null, 2) + '\n');
|
||||
fs.writeFileSync(path.join(dir, 'package-lock.json'), lock('1.0.0.0'));
|
||||
const out = execFileSync('bun', [BIN, 'write', '--version', '1.1.0.0'], { cwd: dir }).toString();
|
||||
expect(JSON.parse(out)).toEqual({ wrote: '1.1.0.0', packageJson: true, packageLock: true });
|
||||
const l = JSON.parse(fs.readFileSync(path.join(dir, 'package-lock.json'), 'utf-8'));
|
||||
expect(l.version).toBe('1.1.0.0');
|
||||
expect(l.packages[''].version).toBe('1.1.0.0');
|
||||
expect(l.packages['node_modules/a'].version).toBe('9.9.9'); // untouched
|
||||
});
|
||||
|
||||
test('repair heals a stale lockfile alongside package.json', () => {
|
||||
fs.writeFileSync(path.join(dir, 'VERSION'), '2.0.0.0\n');
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'x', version: '1.9.0.0' }, null, 2) + '\n');
|
||||
fs.writeFileSync(path.join(dir, 'package-lock.json'), lock('1.9.0.0'));
|
||||
execFileSync('bun', [BIN, 'repair'], { cwd: dir });
|
||||
const l = JSON.parse(fs.readFileSync(path.join(dir, 'package-lock.json'), 'utf-8'));
|
||||
expect(l.version).toBe('2.0.0.0');
|
||||
expect(l.packages[''].version).toBe('2.0.0.0');
|
||||
});
|
||||
|
||||
test('lockfileVersion 1 (no packages map) syncs top-level only, no crash', () => {
|
||||
fs.writeFileSync(path.join(dir, 'VERSION'), '3.0.0.0\n');
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'x', version: '2.9.0.0' }, null, 2) + '\n');
|
||||
fs.writeFileSync(path.join(dir, 'package-lock.json'), JSON.stringify({ name: 'x', version: '2.9.0.0', lockfileVersion: 1 }, null, 2) + '\n');
|
||||
execFileSync('bun', [BIN, 'repair'], { cwd: dir });
|
||||
const l = JSON.parse(fs.readFileSync(path.join(dir, 'package-lock.json'), 'utf-8'));
|
||||
expect(l.version).toBe('3.0.0.0');
|
||||
expect(l.packages).toBeUndefined();
|
||||
});
|
||||
|
||||
test('npm-shrinkwrap.json is synced too when present (never created)', () => {
|
||||
const d2 = fs.mkdtempSync(path.join(os.tmpdir(), 'vbump-shrink-'));
|
||||
fs.writeFileSync(path.join(d2, 'VERSION'), '1.0.0.0\n');
|
||||
fs.writeFileSync(path.join(d2, 'package.json'), JSON.stringify({ name: 'x', version: '1.0.0.0' }, null, 2) + '\n');
|
||||
fs.writeFileSync(path.join(d2, 'npm-shrinkwrap.json'), lock('1.0.0.0').replace('package-lock', 'npm-shrinkwrap'));
|
||||
const out = execFileSync('bun', [BIN, 'write', '--version', '1.1.0.0'], { cwd: d2 }).toString();
|
||||
expect(JSON.parse(out).packageLock).toBe(true);
|
||||
const l = JSON.parse(fs.readFileSync(path.join(d2, 'npm-shrinkwrap.json'), 'utf-8'));
|
||||
expect(l.version).toBe('1.1.0.0');
|
||||
expect(l.packages[''].version).toBe('1.1.0.0');
|
||||
// No package-lock.json invented alongside it.
|
||||
expect(fs.existsSync(path.join(d2, 'package-lock.json'))).toBe(false);
|
||||
fs.rmSync(d2, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('malformed lockfile fails the write with exit 3 (half-write is loud, not silent)', () => {
|
||||
const d3 = fs.mkdtempSync(path.join(os.tmpdir(), 'vbump-badlock-'));
|
||||
fs.writeFileSync(path.join(d3, 'VERSION'), '1.0.0.0\n');
|
||||
fs.writeFileSync(path.join(d3, 'package.json'), JSON.stringify({ name: 'x', version: '1.0.0.0' }, null, 2) + '\n');
|
||||
fs.writeFileSync(path.join(d3, 'package-lock.json'), '{ not json');
|
||||
let code = 0;
|
||||
try { execFileSync('bun', [BIN, 'write', '--version', '1.1.0.0'], { cwd: d3, stdio: 'pipe' }); }
|
||||
catch (e: any) { code = e.status; }
|
||||
expect(code).toBe(3);
|
||||
// VERSION was written before the failure — exactly the half-write the
|
||||
// exit-3 contract exists to surface.
|
||||
expect(fs.readFileSync(path.join(d3, 'VERSION'), 'utf-8').trim()).toBe('1.1.0.0');
|
||||
fs.rmSync(d3, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('classify (idempotency over a real git base)', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vbump-classify-'));
|
||||
afterAll(() => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* noop */ } });
|
||||
|
||||
Reference in New Issue
Block a user