Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- **ci**: `.nvmrc` now pins Node 24, matching the CI matrix. It still read `20`, so `nvm use` handed local developers a runtime that cannot load `node:sqlite` and does not satisfy `commander`'s `engines.node >=22.12.0`.
- **build**: `npm run build` now runs a `clean` step first, removing `dist/` and `tsconfig.tsbuildinfo` before compiling. Without it, `tsc` leaves output for sources that no longer exist, so a file rename could ship stale artifacts — verified reproducible: planting `dist/sync/strings-client.js` and rebuilding left both files in `npm pack` output. Removing the build-info file is required too; deleting only `dist/` makes the incremental compiler report the output as up to date and emit nothing.
- **deps**: `commander` 14.0.3 → 15.0.0. commander 15 is ESM-only (`"type": "module"`) and requires Node >=22.12.0, so it was unmergeable until the Node 24 baseline landed. Jest's `transformIgnorePatterns` allowlist gains `commander` so ts-jest transforms it under CommonJS test execution; without that, 28 suites fail to load with `SyntaxError: Cannot use import statement outside a module`.
- **ci**: Test matrix, release workflow, and security workflow now target Node 24 (previously Node 20 and 22). Node 20 reached end-of-life in April 2026, and Node 24 is the current LTS. This aligns CI with the runtime the project is moving to; the `engines.node` range is unchanged in this entry and is bumped separately.

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
},
"scripts": {
"dev": "ts-node src/cli/index.ts",
"build": "tsc && chmod +x dist/cli/index.js",
"clean": "rm -rf dist tsconfig.tsbuildinfo",
"build": "npm run clean && tsc && chmod +x dist/cli/index.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
Expand Down
71 changes: 71 additions & 0 deletions tests/unit/package-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Tests for package.json publish-surface invariants
*
* These guard properties that are easy to lose in a merge and expensive to
* lose silently: the `clean` script was previously added, lost, and only
* rediscovered when a stale `dist/` was found reaching `npm pack`.
*/

import * as fs from 'fs';
import * as path from 'path';

interface PackageManifest {
scripts: {
clean?: string;
build: string;
prepublishOnly: string;
};
files: string[];
bin: Record<string, string>;
}

describe('package.json manifest', () => {
let pkg: PackageManifest;

beforeAll(() => {
const manifestPath = path.join(__dirname, '..', '..', 'package.json');
pkg = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as PackageManifest;
});

describe('clean script', () => {
it('should define a clean script', () => {
expect(pkg.scripts.clean).toBeDefined();
});

it('should remove dist so renames cannot leak stale output to npm publish', () => {
expect(pkg.scripts.clean).toContain('dist');
});

it('should remove the TypeScript build info file', () => {
// Without this, tsc's incremental cache reports the deleted output as
// up-to-date and emits nothing, leaving dist empty after a clean.
expect(pkg.scripts.clean).toContain('tsbuildinfo');
});
});

describe('build script', () => {
it('should run clean before compiling', () => {
expect(pkg.scripts.build).toMatch(/^npm run clean &&/);
});

it('should make the CLI entrypoint executable', () => {
expect(pkg.scripts.build).toContain('chmod +x');
});
});

describe('publish surface', () => {
it('should rebuild from scratch before publishing', () => {
expect(pkg.scripts.prepublishOnly).toContain('build');
});

it('should exclude source maps and build info from the tarball', () => {
const excluded = pkg.files.filter((entry) => entry.startsWith('!'));
expect(excluded).toContain('!dist/**/*.tsbuildinfo');
expect(excluded).toContain('!dist/**/*.js.map');
});

it('should point bin at a path inside dist', () => {
expect(Object.values(pkg.bin).every((target) => target.startsWith('dist/'))).toBe(true);
});
});
});