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: 5 additions & 0 deletions .changeset/ltrac-466-catalyst-version-field.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": patch
---

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.
63 changes: 63 additions & 0 deletions .github/scripts/__tests__/sync-catalyst-version.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { buildCatalystField, syncCatalystField } from '../sync-catalyst-version.mts';

describe('buildCatalystField', () => {
it('derives version and ref from name + version', () => {
assert.deepEqual(
buildCatalystField({ name: '@bigcommerce/catalyst-core', version: '1.7.0' }),
{ version: '1.7.0', ref: '@bigcommerce/catalyst-core@1.7.0' },
);
});
});

describe('syncCatalystField', () => {
it('adds the catalyst field mirroring version when absent', () => {
const input = `${JSON.stringify(
{ name: '@bigcommerce/catalyst-core', version: '1.8.0' },
null,
2,
)}\n`;

const output = JSON.parse(syncCatalystField(input));

assert.deepEqual(output.catalyst, {
version: '1.8.0',
ref: '@bigcommerce/catalyst-core@1.8.0',
});
});

it('updates a stale catalyst field to the current version', () => {
const input = `${JSON.stringify(
{
name: '@bigcommerce/catalyst-core',
version: '2.0.0',
catalyst: { version: '1.9.0', ref: '@bigcommerce/catalyst-core@1.9.0' },
},
null,
2,
)}\n`;

const output = JSON.parse(syncCatalystField(input));

assert.deepEqual(output.catalyst, {
version: '2.0.0',
ref: '@bigcommerce/catalyst-core@2.0.0',
});
});

it('is idempotent and preserves the trailing newline', () => {
const input = `${JSON.stringify(
{
name: '@bigcommerce/catalyst-core',
version: '1.7.0',
catalyst: { version: '1.7.0', ref: '@bigcommerce/catalyst-core@1.7.0' },
},
null,
2,
)}\n`;

assert.equal(syncCatalystField(input), input);
});
});
58 changes: 58 additions & 0 deletions .github/scripts/sync-catalyst-version.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env node
/* eslint-disable no-console */

import { readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

export interface CorePackageJson {
name: string;
version: string;
catalyst?: { version: string; ref: string };
[key: string]: unknown;
}

// `catalyst.version` mirrors the released version; `catalyst.ref` is the git tag
// (`@bigcommerce/catalyst-core@<version>`) the version corresponds to, consumed
// by the future `catalyst upgrade` command.
export function buildCatalystField(
pkg: Pick<CorePackageJson, "name" | "version">,
): { version: string; ref: string } {
return { version: pkg.version, ref: `${pkg.name}@${pkg.version}` };
}

// Returns the package.json text with its `catalyst` field synced to `version`.
// Re-serializes with the canonical 2-space + trailing-newline format, so a run
// where nothing changed produces no diff.
export function syncCatalystField(packageJsonText: string): string {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const pkg = JSON.parse(packageJsonText) as CorePackageJson;

pkg.catalyst = buildCatalystField(pkg);

return `${JSON.stringify(pkg, null, 2)}\n`;
}

function main(): void {
const corePackageJsonPath = resolve(
dirname(fileURLToPath(import.meta.url)),
"../../core/package.json",
);

const updated = syncCatalystField(readFileSync(corePackageJsonPath, "utf-8"));

writeFileSync(corePackageJsonPath, updated);

// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const { catalyst } = JSON.parse(updated) as CorePackageJson;

console.log(
`Synced core/package.json catalyst field → ${catalyst?.version} (${catalyst?.ref})`,
);
}

const isMain = process.argv[1] === fileURLToPath(import.meta.url);

if (isMain) {
main();
}
3 changes: 3 additions & 0 deletions .github/workflows/changesets-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ jobs:
id: changesets
uses: changesets/action@v1
with:
# Runs `changeset version`, then syncs core/package.json's `catalyst`
# field (version + git ref) to the freshly-bumped version.
version: pnpm changeset:version
publish: pnpm exec changeset publish
title: "Version Packages (`${{ github.ref_name }}`)"
commit: "Version Packages (`${{ github.ref_name }}`)"
Expand Down
4 changes: 4 additions & 0 deletions core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
"name": "@bigcommerce/catalyst-core",
"description": "BigCommerce Catalyst is a Next.js starter kit for building headless BigCommerce storefronts.",
"version": "1.7.0",
"catalyst": {
"version": "1.7.0",
"ref": "@bigcommerce/catalyst-core@1.7.0"
},
"private": true,
"engines": {
"node": ">=24.0.0"
Expand Down
13 changes: 11 additions & 2 deletions core/user-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@ import packageInfo from './package.json';

const commitSha = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA;

const { name, version } = packageInfo;
const { name } = packageInfo;

// Prefer the dedicated `catalyst.version` field, which always reflects the true
// Catalyst release even if a merchant repurposes the top-level `version` (Docker
// labels, deploy tags, etc.). The `?? version` fallback is intentional for
// projects whose package.json predates the `catalyst` field. TypeScript infers
// `catalyst` as always-present from this repo's package.json (so it flags the
// guard as unnecessary), but it can be absent at runtime in older projects.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const catalystVersion = packageInfo.catalyst?.version ?? packageInfo.version;

// Add package name and version to the user agent
// Used as part of API client instantiation
export const backendUserAgent = `${name}/${version}${commitSha ? ` (${commitSha})` : ''}`;
export const backendUserAgent = `${name}/${catalystVersion}${commitSha ? ` (${commitSha})` : ''}`;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"lint": "dotenv -e .env.local -- turbo lint",
"test": "turbo run test",
"test:scripts": "node --test .github/scripts/__tests__/*.test.mts",
"typecheck": "turbo typecheck"
"typecheck": "turbo typecheck",
"changeset:version": "changeset version && node .github/scripts/sync-catalyst-version.mts"
},
"devDependencies": {
"@changesets/changelog-github": "^0.5.1",
Expand Down
Loading