Skip to content

Commit 96b4c3c

Browse files
jorgemoyaclaude
andcommitted
LTRAC-466: feat(core) - Track Catalyst version in a dedicated package.json field
Add a `catalyst` field to core/package.json (`catalyst.version` + `catalyst.ref`) so the true Catalyst version is tracked independently of the top-level `version`, which merchants may repurpose (deploy tags, Docker labels). The backend user-agent now reports `catalyst.version`, falling back to `version` for projects created before the field existed. A release-time sync script (.github/scripts/sync-catalyst-version.mts), wired into the Changesets `version` step via the `changeset:version` root script, keeps `catalyst.version`/`catalyst.ref` in lockstep with each version bump. Refs LTRAC-466 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 226f2f3 commit 96b4c3c

7 files changed

Lines changed: 143 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bigcommerce/catalyst-core": patch
3+
---
4+
5+
Add a `catalyst` field to `core/package.json` (`catalyst.version` and `catalyst.ref`) that tracks the true Catalyst version independently of the top-level `version`, which merchants may repurpose for their own deploy tagging. The backend user-agent now reports `catalyst.version` (falling back to `version` for projects created before the field existed), and the release pipeline keeps the field in sync on each version bump.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
import { buildCatalystField, syncCatalystField } from '../sync-catalyst-version.mts';
5+
6+
describe('buildCatalystField', () => {
7+
it('derives version and ref from name + version', () => {
8+
assert.deepEqual(
9+
buildCatalystField({ name: '@bigcommerce/catalyst-core', version: '1.7.0' }),
10+
{ version: '1.7.0', ref: '@bigcommerce/catalyst-core@1.7.0' },
11+
);
12+
});
13+
});
14+
15+
describe('syncCatalystField', () => {
16+
it('adds the catalyst field mirroring version when absent', () => {
17+
const input = `${JSON.stringify(
18+
{ name: '@bigcommerce/catalyst-core', version: '1.8.0' },
19+
null,
20+
2,
21+
)}\n`;
22+
23+
const output = JSON.parse(syncCatalystField(input));
24+
25+
assert.deepEqual(output.catalyst, {
26+
version: '1.8.0',
27+
ref: '@bigcommerce/catalyst-core@1.8.0',
28+
});
29+
});
30+
31+
it('updates a stale catalyst field to the current version', () => {
32+
const input = `${JSON.stringify(
33+
{
34+
name: '@bigcommerce/catalyst-core',
35+
version: '2.0.0',
36+
catalyst: { version: '1.9.0', ref: '@bigcommerce/catalyst-core@1.9.0' },
37+
},
38+
null,
39+
2,
40+
)}\n`;
41+
42+
const output = JSON.parse(syncCatalystField(input));
43+
44+
assert.deepEqual(output.catalyst, {
45+
version: '2.0.0',
46+
ref: '@bigcommerce/catalyst-core@2.0.0',
47+
});
48+
});
49+
50+
it('is idempotent and preserves the trailing newline', () => {
51+
const input = `${JSON.stringify(
52+
{
53+
name: '@bigcommerce/catalyst-core',
54+
version: '1.7.0',
55+
catalyst: { version: '1.7.0', ref: '@bigcommerce/catalyst-core@1.7.0' },
56+
},
57+
null,
58+
2,
59+
)}\n`;
60+
61+
assert.equal(syncCatalystField(input), input);
62+
});
63+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env node
2+
/* eslint-disable no-console */
3+
4+
import { readFileSync, writeFileSync } from "node:fs";
5+
import { dirname, resolve } from "node:path";
6+
import { fileURLToPath } from "node:url";
7+
8+
export interface CorePackageJson {
9+
name: string;
10+
version: string;
11+
catalyst?: { version: string; ref: string };
12+
[key: string]: unknown;
13+
}
14+
15+
// `catalyst.version` mirrors the released version; `catalyst.ref` is the git tag
16+
// (`@bigcommerce/catalyst-core@<version>`) the version corresponds to, consumed
17+
// by the future `catalyst upgrade` command.
18+
export function buildCatalystField(
19+
pkg: Pick<CorePackageJson, "name" | "version">,
20+
): { version: string; ref: string } {
21+
return { version: pkg.version, ref: `${pkg.name}@${pkg.version}` };
22+
}
23+
24+
// Returns the package.json text with its `catalyst` field synced to `version`.
25+
// Re-serializes with the canonical 2-space + trailing-newline format, so a run
26+
// where nothing changed produces no diff.
27+
export function syncCatalystField(packageJsonText: string): string {
28+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
29+
const pkg = JSON.parse(packageJsonText) as CorePackageJson;
30+
31+
pkg.catalyst = buildCatalystField(pkg);
32+
33+
return `${JSON.stringify(pkg, null, 2)}\n`;
34+
}
35+
36+
function main(): void {
37+
const corePackageJsonPath = resolve(
38+
dirname(fileURLToPath(import.meta.url)),
39+
"../../core/package.json",
40+
);
41+
42+
const updated = syncCatalystField(readFileSync(corePackageJsonPath, "utf-8"));
43+
44+
writeFileSync(corePackageJsonPath, updated);
45+
46+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
47+
const { catalyst } = JSON.parse(updated) as CorePackageJson;
48+
49+
console.log(
50+
`Synced core/package.json catalyst field → ${catalyst?.version} (${catalyst?.ref})`,
51+
);
52+
}
53+
54+
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
55+
56+
if (isMain) {
57+
main();
58+
}

.github/workflows/changesets-release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ jobs:
4343
id: changesets
4444
uses: changesets/action@v1
4545
with:
46+
# Runs `changeset version`, then syncs core/package.json's `catalyst`
47+
# field (version + git ref) to the freshly-bumped version.
48+
version: pnpm changeset:version
4649
publish: pnpm exec changeset publish
4750
title: "Version Packages (`${{ github.ref_name }}`)"
4851
commit: "Version Packages (`${{ github.ref_name }}`)"

core/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
"name": "@bigcommerce/catalyst-core",
33
"description": "BigCommerce Catalyst is a Next.js starter kit for building headless BigCommerce storefronts.",
44
"version": "1.7.0",
5+
"catalyst": {
6+
"version": "1.7.0",
7+
"ref": "@bigcommerce/catalyst-core@1.7.0"
8+
},
59
"private": true,
610
"engines": {
711
"node": ">=24.0.0"

core/user-agent.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import packageInfo from './package.json';
22

33
const commitSha = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA;
44

5-
const { name, version } = packageInfo;
5+
const { name } = packageInfo;
6+
7+
// Prefer the dedicated `catalyst.version` field, which always reflects the true
8+
// Catalyst release even if a merchant repurposes the top-level `version` (Docker
9+
// labels, deploy tags, etc.). Falls back to `version` for projects created
10+
// before the `catalyst` field existed.
11+
const catalystVersion = packageInfo.catalyst?.version ?? packageInfo.version;
612

713
// Add package name and version to the user agent
814
// Used as part of API client instantiation
9-
export const backendUserAgent = `${name}/${version}${commitSha ? ` (${commitSha})` : ''}`;
15+
export const backendUserAgent = `${name}/${catalystVersion}${commitSha ? ` (${commitSha})` : ''}`;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"lint": "dotenv -e .env.local -- turbo lint",
2020
"test": "turbo run test",
2121
"test:scripts": "node --test .github/scripts/__tests__/*.test.mts",
22-
"typecheck": "turbo typecheck"
22+
"typecheck": "turbo typecheck",
23+
"changeset:version": "changeset version && node .github/scripts/sync-catalyst-version.mts"
2324
},
2425
"devDependencies": {
2526
"@changesets/changelog-github": "^0.5.1",

0 commit comments

Comments
 (0)