Skip to content

feat(cli): TRAC-1536 Reconcile Catalyst dependency versions on upgrade - #3192

Merged
chanceaclark merged 1 commit into
canaryfrom
chancellorclark/ltrac-1630-explore-dependency-migration-during-catalyst-upgrades
Aug 26, 2026
Merged

feat(cli): TRAC-1536 Reconcile Catalyst dependency versions on upgrade#3192
chanceaclark merged 1 commit into
canaryfrom
chancellorclark/ltrac-1630-explore-dependency-migration-during-catalyst-upgrades

Conversation

@chanceaclark

@chanceaclark chanceaclark commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Jira: TRAC-1536

What/Why?

catalyst upgrade never moved @bigcommerce/catalyst* dependency versions, for either project layout. Chris hit this upgrading a monorepo-structure project from 1.6.x to 1.7.x.

It isn't a bug in the merge. @bigcommerce/catalyst-core is private, so it's tagged but never published, which means pnpm's publish-time rewrite never runs on the tagged tree and every tag still says workspace:^ in core/package.json. Both sides of the 3-way merge agree on those lines, so the merge correctly leaves the project's versions alone — forever. Flat projects keep whatever catalyst create resolved at scaffold time; monorepo projects keep workspace:^.

Caret ranges pick up minor and patch drift on the next install anyway, so this is mostly pre-emptive. What it guards against is a major bump of a Catalyst package, where a project created on 1.6.x would sit on the wrong major with nothing telling it so.

How it works

Each tag's tarball already contains packages/*/package.json, so the versions that shipped with a given release are readable straight from it. downloadCore now grabs them on the way past and caches them alongside the tree. That's the only accurate source: these packages version independently of core, so the npm latest lookup used elsewhere answers for today rather than for the release being downloaded.

Instead of patching up the merge result afterwards, the downloaded base and target manifests are pinned to their own versions before merging. The existing 3-way merge then carries the bump like any other change, and conflicts if the project pinned something deliberately. Nothing downstream needed to change.

Two decisions worth a look:

  • The rewrite edits the text, it doesn't round-trip the JSON. Re-stringifying would reformat the whole manifest and desync it from the project's copy, turning a one-line dependency bump into a whole-file conflict. Editing the text also survives a package.json that still has conflict markers elsewhere.
  • Dependencies still on workspace: are left alone on both sides. Changing just one side would invent a conflict on something that works fine, and staying put is a reasonable choice. Those get an opt-in prompt instead: --yes accepts it, and --dry-run or a non-interactive run just prints the versions. The swap is additionally gated on package.json having merged cleanly, since a text rewrite of a file full of conflict markers could land inside a hunk nobody has reviewed yet.

Also here

Two nudges the merge can't cover on its own:

  • Reminding you to reinstall when the upgrade changed package.json. The command has never run an install or mentioned one, so the lockfile was quietly left behind.
  • Flagging when the project's @bigcommerce/catalyst is out of date. catalyst create pins it exactly, and it doesn't exist upstream, so no merge can move it — the CLI doing the upgrading goes stale permanently. It stays quiet when the range already admits the published version, so a caret range doesn't nag.

Follow-up, kept separate

The create path has the same root cause: it resolves workspace deps against npm latest rather than the release being extracted, so catalyst create --gh-ref <old tag> pulls in today's versions. Different code path, tracked on its own ticket.

Testing

pnpm test, pnpm lint, and pnpm typecheck all pass in packages/catalyst (660 tests).

New coverage:

  • Unit: the text rewrite, the per-dependency decisions (real range / workspace: / package absent from a release / missing manifest), the migration itself, the out-of-date CLI check against a new registry.npmjs.org mock, and the lockfile fallback.
  • Action: a test.each table walking the prompt gate — accepted, declined, --yes, non-interactive — plus --dry-run asserting the project is untouched.
  • Integration: the real 1.6.3 and 1.7.0 tags happen to cross an actual @bigcommerce/catalyst-client 1.0.1 to 1.0.2 bump, so that's asserted end to end against live tarballs on both merge engines.

Verified by hand

Both layouts, against real tarballs, using the built CLI. Setup is self-contained — no store credentials needed, since it stands in for what catalyst create writes:

mkdir flat && cd flat
curl -sL "https://api.github.com/repos/bigcommerce/catalyst/tarball/%40bigcommerce%2Fcatalyst-core%401.6.3" -o t.tgz
tar -xzf t.tgz --strip-components=2 '*/core/' && rm t.tgz

# what `catalyst create` would have written: workspace specifiers resolved,
# `private` dropped, provenance stamped
node -e "
const fs=require('fs');
const p=JSON.parse(fs.readFileSync('package.json','utf8'));
p.dependencies['@bigcommerce/catalyst-client']='^1.0.1';
p.devDependencies['@bigcommerce/eslint-config-catalyst']='^1.0.0';
delete p.private;
p.catalyst={version:'1.6.3',ref:'@bigcommerce/catalyst-core@1.6.3'};
fs.writeFileSync('package.json', JSON.stringify(p,null,2)+'\n');
"
touch pnpm-lock.yaml && git init -q . && git add -A && git commit -qm "catalyst 1.6.3"

catalyst upgrade 1.7.0

1. Flat project, dependency already on a real range. The bump rides the merge:

◢ @bigcommerce/catalyst v1.2.0

✔ Downloaded both versions.
⚠ Merged with conflicts — resolve the markers, then commit.
✔ catalyst.ref updated → @bigcommerce/catalyst-core@1.7.0
ℹ package.json still has conflicts in other sections — resolve them, then: git add package.json

51 updated, 2 added, 0 removed, 1 with conflicts

 WARN
Staged the clean changes. 1 file(s) need conflict resolution (the <<<ours/===/theirs>>> markers):
  package.json

Resolve them, then: git add . && git commit && pnpm install

The resulting package.json — the dependency merged cleanly to ^1.0.2 even though the manifest conflicts elsewhere:

  1  {
  2    "name": "@bigcommerce/catalyst-core",
  3    "description": "BigCommerce Catalyst is a Next.js starter kit ...",
  4  <<<<<<< ours
  5    "version": "1.6.3",
  6  =======
  7    "version": "1.7.0",
  8    "private": true,
  9  >>>>>>> theirs
 ...
 24      "@bigcommerce/catalyst-client": "^1.0.2",      was ^1.0.1

That remaining conflict is pre-existing and unrelated: catalyst create drops private, so the version/private hunk always diverges from upstream. Worth noting because it's the common case, which is why the install reminder is chained onto the resolution step (git commit && pnpm install) rather than printed on its own — running an install against a manifest full of conflict markers just fails.

2. Old monorepo layout (core/ nested), --dry-run. Reports the workspace references and writes nothing:

 WARN  Catalyst dependencies still using the workspace protocol (2):
  @bigcommerce/catalyst-client         workspace:^ → ^1.0.2
  @bigcommerce/eslint-config-catalyst  workspace:^ → ^1.0.0

They resolve to your local packages/ copies, which catalyst upgrade does not update — so they never receive upgrades.
$ git status --porcelain | wc -l
       0

3. Same project, catalyst upgrade 1.7.0 --yes. Clean merge, so the migration applies:

✔ Merged cleanly.
✔ catalyst.ref updated → @bigcommerce/catalyst-core@1.7.0

 WARN  Catalyst dependencies still using the workspace protocol (2):
  @bigcommerce/catalyst-client         workspace:^ → ^1.0.2
  @bigcommerce/eslint-config-catalyst  workspace:^ → ^1.0.0

They resolve to your local packages/ copies, which catalyst upgrade does not update — so they never receive upgrades.

✔ Migrated 2 dependencies to published versions.

52 updated, 2 added, 0 removed
ℹ Dependencies changed — run pnpm install to update your lockfile.
✔ Staged all changes — review with git diff --cached, then commit.
ℹ Your Catalyst CLI is behind (1.1.0 → 1.2.0). Update it with pnpm add -D @bigcommerce/catalyst@1.2.0.
$ git diff --cached core/package.json
-    "@bigcommerce/catalyst-client": "workspace:^",
+    "@bigcommerce/catalyst-client": "^1.0.2",
-    "@bigcommerce/eslint-config-catalyst": "workspace:^",
+    "@bigcommerce/eslint-config-catalyst": "^1.0.0",

4. Safety gate. Running the same --yes against a variant whose package.json came back conflicted correctly refuses to rewrite it:

ℹ Keeping the workspace references. Switching abandons any local packages/ copy you've customized; otherwise apply the versions above in package.json and reinstall.

5. No false nagging. A project on "@bigcommerce/catalyst": "^1.2.0" with 1.2.0 published gets no CLI advisory at all, since the range already admits it. The exact pin in scenario 3 (1.1.0) does get one.

Bugs found while doing the above

Both fixed here:

  1. semver.minVersion throws rather than returning null on anything that isn't a semver range, so a tarball path or git URL dependency would have crashed the check.
  2. nypm's package-manager detection throws when package.json still has conflict markers — scenario 1's exact situation — which killed the run before the summary printed. Detection now reads the lockfile first, which is why scenarios 1 and 3 correctly say pnpm install and not npm install.

Migration

None. Nothing moved and nothing breaks; this is additive to an existing command.

Refs TRAC-1536

@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: be22892

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@bigcommerce/catalyst Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
catalyst Ready Ready Preview Aug 26, 2026 3:58pm

Request Review

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Report

Comparing against baseline from edb8f97 (2026-08-26).

No bundle size changes detected.

@chanceaclark
chanceaclark force-pushed the chancellorclark/ltrac-1630-explore-dependency-migration-during-catalyst-upgrades branch from 5b3dda6 to 05cf895 Compare August 25, 2026 16:29
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Unlighthouse Performance Comparison — Vercel

Comparing PR preview deployment Unlighthouse scores vs production Unlighthouse scores.

Summary Score

Aggregate score across all categories as reported by Unlighthouse.

Prod Desktop Prod Mobile Preview Desktop Preview Mobile
Score 90 92 92 94

Category Scores

Category Prod Desktop Prod Mobile Preview Desktop Preview Mobile
Performance 76 77 72 76
Accessibility 95 98 95 98
Best Practices 100 100 100 100
SEO 88 100 88 100

Core Web Vitals

Metric Prod Desktop Prod Mobile Preview Desktop Preview Mobile
LCP 3.7 s 4.2 s 4.4 s 6.8 s
CLS 0.001 0.183 0.04 0
FCP 1.2 s 1.2 s 1.2 s 1.2 s
TBT 0 ms 0 ms 0 ms 0 ms
Max Potential FID 50 ms 50 ms 50 ms 50 ms
Time to Interactive 3.7 s 4.2 s 4.4 s 6.8 s

Full Unlighthouse report →

@chanceaclark
chanceaclark marked this pull request as ready for review August 25, 2026 17:13
@chanceaclark
chanceaclark requested a review from a team as a code owner August 25, 2026 17:13
@chanceaclark
chanceaclark force-pushed the chancellorclark/ltrac-1630-explore-dependency-migration-during-catalyst-upgrades branch from 05cf895 to f41f183 Compare August 25, 2026 21:53

@jorgemoya jorgemoya left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@chanceaclark
chanceaclark force-pushed the chancellorclark/ltrac-1630-explore-dependency-migration-during-catalyst-upgrades branch from f41f183 to f7bdff6 Compare August 26, 2026 15:32
Every Catalyst tag ships core/package.json with `workspace:` specifiers,
because @bigcommerce/catalyst-core is private and pnpm's publish-time
rewrite never runs on the tagged tree. Both sides of the 3-way merge
therefore agreed on those lines, so a project's @bigcommerce/catalyst*
dependency versions never moved through an upgrade: flat projects kept
whatever `catalyst create` resolved at scaffold time, and monorepo-
structure projects kept `workspace:^` indefinitely.

Read the versions each tag actually published from that tag's own
tarball and pin the downloaded base and target trees to them, so the
existing merge carries the bump like any other change, or conflicts when
the merchant pinned deliberately. A dependency the project still holds
as `workspace:` is skipped on both sides, since normalizing one side
alone would manufacture a conflict on something that works fine as-is;
those are surfaced instead as an opt-in migration prompt.

Also add two advisories the merge cannot cover: a reminder to reinstall
when the upgrade changed package.json, and a notice when the pinned
@bigcommerce/catalyst is behind the published version, which it always
will be eventually since `catalyst create` pins it exactly and the
dependency does not exist upstream for a merge to touch.

Refs TRAC-1536
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chanceaclark
chanceaclark force-pushed the chancellorclark/ltrac-1630-explore-dependency-migration-during-catalyst-upgrades branch from f7bdff6 to be22892 Compare August 26, 2026 15:57
@chanceaclark
chanceaclark added this pull request to the merge queue Aug 26, 2026
Merged via the queue into canary with commit d263871 Aug 26, 2026
17 of 18 checks passed
@chanceaclark
chanceaclark deleted the chancellorclark/ltrac-1630-explore-dependency-migration-during-catalyst-upgrades branch August 26, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants