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
5 changes: 4 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["oidc-devtools", "@wolfcola/e2e", "@wolfcola/devtools-ui"]
"ignore": ["oidc-devtools", "@wolfcola/e2e", "@wolfcola/devtools-ui"],
"privatePackages": {
"version": true
}
}
7 changes: 7 additions & 0 deletions .changeset/sync-manifest-version.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@wolfcola/devtools-extension': patch
---

Automate manifest.json version sync: after `changeset version` bumps
package.json, the new `sync-manifest` CLI copies the version into
manifest.json so Chrome Web Store publishes show real version numbers.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# @wolfcola/changeset-sync-manifest Design

**Problem:** The Chrome extension `manifest.json` version is hardcoded at `0.1.0` and never bumped by changesets. Every CWS publish overwrites the same display version.

**Solution:** A small CLI package that runs after `changeset version` and copies the `package.json` version into `manifest.json` for a given package directory.

## Package

- **Name:** `@wolfcola/changeset-sync-manifest`
- **Location:** `packages/changeset-sync-manifest`
- **Private:** yes

### Files

| File | Purpose |
| ---------------------- | -------------------------------------------------------------------- |
| `package.json` | Package manifest with `bin` entry |
| `bin/sync-manifest.js` | Compiled CLI entry point |
| `src/sync.ts` | Pure function: read `package.json` version, write to `manifest.json` |
| `src/sync.test.ts` | Unit tests |

### CLI Interface

```
sync-manifest <dir>
```

- `<dir>` — path to a package directory containing both `package.json` and `manifest.json`
- Reads `<dir>/package.json` → extracts `version`
- Reads `<dir>/manifest.json` → sets `version` field → writes back
- Exits non-zero if either file is missing or JSON is malformed

### Pure Function

```ts
syncManifestVersion(dir: string): void
```

Reads `package.json` and `manifest.json` from `dir`, copies the version, writes `manifest.json` back with the updated version. Preserves existing formatting (2-space indent, trailing newline).

## Integration

### Changesets config (`.changeset/config.json`)

- Add `"privatePackages": { "version": true }` so changesets bumps private packages
- Remove `@wolfcola/devtools-extension` from `ignore` so it participates in the `@wolfcola/*` fixed group

### Version script (root `package.json`)

```
"version": "changeset version && sync-manifest packages/devtools-extension && prettier --write '**/package.json' pnpm-workspace.yaml"
```

### Build pipeline (unchanged)

`build.mjs` continues reading `manifest.json` and calling `stampVersion()` to append the CI build number as the 4th version segment. The only difference is that the base version in `manifest.json` now reflects the real package version instead of a hardcoded `0.1.0`.

## What this does NOT do

- Does not handle the VS Code extension (its `package.json` is its manifest — changesets handles it directly if removed from `ignore`)
- Does not scan the workspace automatically — takes an explicit directory argument
- Does not handle jsonpath or arbitrary file targets — just `package.json` → `manifest.json` version sync
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"test": "vitest run",
"typecheck": "tsc --build",
"changeset": "changeset",
"version": "changeset version && prettier --write '**/package.json' pnpm-workspace.yaml",
"version": "changeset version && sync-manifest packages/devtools-extension && prettier --write '**/package.json' pnpm-workspace.yaml",
"release": "pnpm build && changeset publish",
"syncpack:lint": "syncpack lint",
"syncpack:fix": "syncpack fix-mismatches",
Expand All @@ -45,6 +45,7 @@
"syncpack": "^15.1.2",
"typescript": "5.8.3",
"vite": "catalog:vite",
"@wolfcola/changeset-sync-manifest": "workspace:*",
"vitest": "catalog:vitest"
},
"pnpm": {
Expand Down
24 changes: 24 additions & 0 deletions packages/changeset-sync-manifest/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@wolfcola/changeset-sync-manifest",
"version": "0.0.0",
"description": "Sync package.json version into manifest.json after changeset version",
"license": "MIT",
"type": "module",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/ryanbas21/devtools.git",
"directory": "packages/changeset-sync-manifest"
},
"bin": {
"sync-manifest": "./dist/bin.js"
},
"scripts": {
"build": "tsc -p tsconfig.lib.json",
"lint": "eslint .",
"test": "vitest run"
},
"devDependencies": {
"vitest": "catalog:vitest"
}
}
18 changes: 18 additions & 0 deletions packages/changeset-sync-manifest/src/bin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env node
import { resolve } from 'node:path';
import { syncManifestVersion } from './sync.js';

const dir = process.argv[2];

if (!dir) {
console.error('Usage: sync-manifest <dir>');
process.exit(1);
}

try {
syncManifestVersion(resolve(dir));
console.log(`Synced manifest.json version in ${dir}`);
} catch (e) {
console.error(String(e instanceof Error ? e.message : e));
process.exit(1);
}
75 changes: 75 additions & 0 deletions packages/changeset-sync-manifest/src/sync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { syncManifestVersion } from './sync.js';

describe('syncManifestVersion', () => {
let dir: string;

beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'sync-manifest-'));
});

afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});

it('copies version from package.json to manifest.json', () => {
writeFileSync(join(dir, 'package.json'), JSON.stringify({ version: '2.0.0' }));
writeFileSync(
join(dir, 'manifest.json'),
JSON.stringify({ manifest_version: 3, version: '0.1.0' }),
);

syncManifestVersion(dir);

const manifest = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8'));
expect(manifest.version).toBe('2.0.0');
});

it('preserves other manifest fields', () => {
writeFileSync(join(dir, 'package.json'), JSON.stringify({ version: '1.0.0' }));
writeFileSync(
join(dir, 'manifest.json'),
JSON.stringify({ manifest_version: 3, name: 'My Extension', version: '0.1.0' }),
);

syncManifestVersion(dir);

const manifest = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8'));
expect(manifest.manifest_version).toBe(3);
expect(manifest.name).toBe('My Extension');
expect(manifest.version).toBe('1.0.0');
});

it('writes with 2-space indent and trailing newline', () => {
writeFileSync(join(dir, 'package.json'), JSON.stringify({ version: '1.0.0' }));
writeFileSync(join(dir, 'manifest.json'), JSON.stringify({ version: '0.1.0' }));

syncManifestVersion(dir);

const raw = readFileSync(join(dir, 'manifest.json'), 'utf8');
expect(raw).toContain(' "version"');
expect(raw.endsWith('\n')).toBe(true);
});

it('throws if package.json is missing', () => {
writeFileSync(join(dir, 'manifest.json'), JSON.stringify({ version: '0.1.0' }));

expect(() => syncManifestVersion(dir)).toThrow('package.json');
});

it('throws if manifest.json is missing', () => {
writeFileSync(join(dir, 'package.json'), JSON.stringify({ version: '1.0.0' }));

expect(() => syncManifestVersion(dir)).toThrow('manifest.json');
});

it('throws if package.json has no version field', () => {
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'test' }));
writeFileSync(join(dir, 'manifest.json'), JSON.stringify({ version: '0.1.0' }));

expect(() => syncManifestVersion(dir)).toThrow('version');
});
});
31 changes: 31 additions & 0 deletions packages/changeset-sync-manifest/src/sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

export const syncManifestVersion = (dir: string): void => {
const pkgPath = join(dir, 'package.json');
const manifestPath = join(dir, 'manifest.json');

let pkgRaw: string;
try {
pkgRaw = readFileSync(pkgPath, 'utf8');
} catch {
throw new Error(`Cannot read package.json at ${pkgPath}`);
}

let manifestRaw: string;
try {
manifestRaw = readFileSync(manifestPath, 'utf8');
} catch {
throw new Error(`Cannot read manifest.json at ${manifestPath}`);
}

const pkg = JSON.parse(pkgRaw) as { version?: string };
if (!pkg.version) {
throw new Error(`No version field in ${pkgPath}`);
}

const manifest = JSON.parse(manifestRaw) as Record<string, unknown>;
manifest.version = pkg.version;

writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
};
19 changes: 19 additions & 0 deletions packages/changeset-sync-manifest/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
"module": "nodenext",
"moduleResolution": "nodenext",
"verbatimModuleSyntax": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"references": [],
"exclude": ["vitest.config.mts", "src/**/*.test.ts"]
}
6 changes: 6 additions & 0 deletions packages/changeset-sync-manifest/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": false
}
}
14 changes: 14 additions & 0 deletions packages/changeset-sync-manifest/vitest.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config';

export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/changeset-sync-manifest',
test: {
name: 'changeset-sync-manifest',
watch: false,
globals: true,
environment: 'node',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
},
}));
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
{ "path": "packages/devtools-extension" },
{ "path": "packages/vscode-extension" },
{ "path": "packages/treeshake-check" },
{ "path": "packages/eslint-plugin-treeshake" }
{ "path": "packages/eslint-plugin-treeshake" },
{ "path": "packages/changeset-sync-manifest" }
]
}
Loading