Version Packages (canary) - #3135
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
Author
Unlighthouse Performance Comparison — VercelComparing PR preview deployment Unlighthouse scores vs production Unlighthouse scores. Summary ScoreAggregate score across all categories as reported by Unlighthouse.
Category Scores
Core Web Vitals
|
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
July 27, 2026 15:58
00ab317 to
4b49e3a
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
July 29, 2026 14:12
4b49e3a to
723c090
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 3, 2026 15:43
723c090 to
d1c3c19
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 4, 2026 18:49
d1c3c19 to
a55780a
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 5, 2026 14:44
a55780a to
a2ac867
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 5, 2026 14:53
a2ac867 to
3eea6a4
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 5, 2026 16:18
3eea6a4 to
5a23986
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 5, 2026 18:49
5a23986 to
1a31d5b
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 5, 2026 20:42
1a31d5b to
a67e4d3
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 7, 2026 11:23
a67e4d3 to
534e059
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 7, 2026 16:57
534e059 to
0d90b1a
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 12, 2026 15:12
0d90b1a to
30f05e5
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 18, 2026 16:10
30f05e5 to
1ed150c
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 24, 2026 21:59
689788e to
a6977b7
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 25, 2026 18:51
a6977b7 to
306c7f6
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 26, 2026 14:55
306c7f6 to
6707c9d
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 26, 2026 17:21
6707c9d to
7e8c33b
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 26, 2026 20:57
7e8c33b to
f834d9e
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 26, 2026 21:05
f834d9e to
e3fb587
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 27, 2026 17:59
e3fb587 to
c2ae56a
Compare
github-actions
Bot
force-pushed
the
changeset-release/canary
branch
from
August 27, 2026 18:17
c2ae56a to
6fac11d
Compare
Contributor
Author
Bundle Size ReportComparing against baseline from No bundle size changes detected. |
jorgemoya
approved these changes
Aug 27, 2026
jorgemoya
enabled auto-merge
August 27, 2026 21:29
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to canary, this PR will be updated.
Releases
@bigcommerce/catalyst@1.3.0
Minor Changes
#3156
9990a87Thanks @jordanarldt! - Print the DNS records to publish whencatalyst domains addsucceeds. The A and CNAME values that point the domain at the project are shown with the success message, along with which to publish and a note that they are only returned when the domain is added. The records survive--wait, and are omitted when the API has none to share yet.#3166
382bdf5Thanks @jordanarldt! - Standardize resource commands on plural names:catalyst projectis nowcatalyst projects, andcatalyst channelis nowcatalyst channels, matching the already-pluraldomainsandlogs. The singular form of every resource command remains as an alias —project,channel,domain, andlogall still resolve — so existing scripts keep working. Telemetry continues to report the canonical plural name regardless of which form was typed.#3192
d263871Thanks @chanceaclark! -catalyst upgradenow keeps your@bigcommerce/catalyst*dependencies up to date.Until now these versions never moved during an upgrade, so a project stayed pinned to whatever it was created with. The upgrade now brings them to the versions that shipped with the release you're upgrading to, handled like any other change: applied automatically, or flagged as a conflict if you'd pinned one on purpose.
If your project still references these packages with
workspace:^, the upgrade offers to swap them for published versions so your package manager can keep them current from then on. Declining, running without a terminal, or using--dry-runchanges nothing;--yesaccepts.Two new reminders round it out: run an install when the upgrade touched your
package.json, and update@bigcommerce/catalystitself when a newer version is out.Patch Changes
#3164
eef0c18Thanks @jordanarldt! - Stop asking users to log in again aftercatalyst create. Credentials from the initial authentication are now written to the new project's.bigcommerce/project.jsonon every scaffold, not just--hosting commerce, socatalyst deployno longer fails with "Missing credentials" andcatalyst project createno longer re-prompts for login.#3167
2ec54dfThanks @jordanarldt! - Print every request undercatalyst logs tail --format request, including requests that emitted no log messages. Previously each line was tied to a log entry, so a request that logged nothing disappeared from the stream. Therequestformat now reads[timestamp] METHOD URL (status) [LEVEL] message, moving the level after the request details in bothlogs tailandlogs query.catalyst logs tail --helpnow documents each format and notes thatdefaultandshortonly show requests with a message body.#3193
391f96cThanks @jorgemoya! - Replace the Durable Object revalidation queue with a self-fetch queue, so ISR revalidation can work on native hosting at all.This does not make anything faster, and changes no behavior today. Catalyst currently ships no route with a revalidate window, so the queue is never invoked. From the built prerender manifest, every prerendered route is
initialRevalidateSeconds: falseanddynamicRoutesis empty. Thenext: { revalidate }options on the product and faceted-search queries are fetch-level data caching and do not feed this queue.What this fixes is a trap rather than a slowdown: with the previous config, the first route to adopt ISR would have failed to revalidate silently, because the error thrown below is an
IgnorableError(logLevel = 0, dropped by OpenNext's logger under the default threshold). Pages would have gone permanently stale with nothing in the logs.OpenNext's
doQueueroutes revalidation through a Durable Object whose constructor readsenv.WORKER_SELF_REFERENCEand throws without it:That binding cannot exist on native hosting. A Cloudflare
servicebinding resolves against account-level Workers, and a Catalyst deployment is a script inside a dispatch namespace, which is not addressable that way. Adding it was attempted and rejected at upload:The
dispatch_namespacebinding sometimes suggested as the alternative is worse: OpenNext calls.fetch()directly on the value while that binding exposes.get(name), and.get()accepts any script in the namespace — binding it into a tenant Worker would let any deployment invoke any other deployment's Worker.What changed
Revalidation does not require a binding. It is a
HEADrequest to the page's own public URL carrying the build-time preview secret — exactly what the Durable Object issues once it holds the service handle.queuenow uses a small self-contained queue that issues that request with a plainfetch, which leaves and re-enters through the dispatch router and arrives at the same Worker.global_fetch_strictly_publicis already set, so the subrequest is not short-circuited internally.It remains wrapped in
queueCache, so concurrent stale hits for one path still collapse into a single revalidation.Trade-off: the Durable Object's retry and max-concurrency handling is lost. A failed revalidation is retried on the next stale hit rather than by the queue itself, and revalidations are no longer capped at a concurrency limit.
The
NEXT_CACHE_DO_QUEUEbinding is intentionally left in place. OpenNext's worker template exports all three Durable Object classes unconditionally, so it still resolves and needs no Durable Object migration; it is simply inert.#3183
ce7d1b2Thanks @jorgemoya! - Restore tag checking on regional cache hits, replacing a CDN cache purge that never worked.cachePurgewas declared in the generatedopen-next.config.tsbut never had credentials on native hosting, so every purge attempt no-opped withNo cache zone ID or API token provided. Skipping cache purge.The declaration alone was harmful: OpenNext'sisPurgeCacheEnabled()only checks whethercachePurgeis declared, not whether it works. Believing purge was handling invalidation, it disabledshouldLazilyUpdateOnCacheHit— documented as on by default for'long-lived'mode — and Catalyst additionally setbypassTagCacheOnCacheHit: true.A regional (Cache API) hit was therefore neither purged, nor refreshed from R2, nor checked against the tag cache. Stale data was served for the full
max-agewindow (the route'srevalidate, or a 30-minute default), andrevalidateTagcalls landing in that window had no effect on it.Purge and tag-checking are alternatives, and we had neither
doShardedTagCache.writeTags()always writes the revalidation time to its Durable Object shards and always clears the regional tag cache. Only the CDN purge is gated behindisPurgeCacheEnabled(). So tag invalidation was already durable — purge exists solely to evict incremental cache entries held in the Cache API, which is exactly the checkbypassTagCacheOnCacheHitwas skipping.Either mechanism delivers correct invalidation: purge evicts the entries, or the tag cache is consulted on hits. Catalyst was configured for the first and got neither.
What changed
bypassTagCacheOnCacheHit: true, so the tag cache is consulted on a regional hit. The OpenNext docs require this option be paired with working purge: "make sure that the cache gets purged either by enabling the auto cache purging feature or manually."cachePurge: purgeCache({ type: 'durableObject' }), restoringshouldLazilyUpdateOnCacheHitto its documented default so a hit also refreshes from R2 in the background.The trade is an extra tag-cache read and R2 read on a cache hit, which is what those options were exchanging for correctness. Invalidation via
revalidateTagnow actually takes effect on regional cache hits.Why purge was not simply fixed
Purge requires a Cloudflare API token bound into the Worker, and a Worker binding is readable by the merchant's own application code. Cloudflare purge is zone-scoped and native hosting places all tenants on one shared zone, so a token extracted from any tenant could purge every other tenant's cache. Scoping it to Cache Purge alone reduces the severity but does not remove it.
Instant invalidation via purge remains worth having — it is faster than tag-checking and avoids the extra reads. Restoring it needs a design that keeps the credential out of tenant Workers, such as routing purge through a platform-owned worker or an authenticated service endpoint with per-tenant authorization.
The
NEXT_CACHE_DO_PURGEDurable Object binding is intentionally left in place. OpenNext's worker template exports all three DO classes unconditionally, so the binding still resolves and no Durable Object migration is required; it is simply inert until purge returns.#3184
5f7e630Thanks @jorgemoya! - Ship the_headersfile so hashed static assets get an immutableCache-Control, instead of being revalidated on every repeat page view.packages/catalyst/templates/public_headershas always specified the right policy for/_next/static/*, but nothing ever copied it into the build output —build.tswrote onlyopen-next.config.tsandwrangler.jsonc, and no_headersfile existed anywhere in the repo.Without it, Workers Assets serves those files with its own default. Measured on a deployed store, all 32 hashed assets on a product page returned:
max-age=0, must-revalidateon a content-hashed filename is wrong by construction: the hash is the version, so a given URL can never return different bytes. The header forced browsers to issue a conditional request for all 32 assets on every repeat view (~75ms each, all answered304 Not Modified), including 4 render-blocking CSS files and 3 fonts. With the intendedpublic,max-age=31536000,immutable, those requests disappear entirely.The reason the header was missing at all is that
/_next/static/*never reaches the Next.js server on Workers — Cloudflare's asset layer serves it directly, so Next's own immutable header never applies._headersis the supported override, and OpenNext'smigratecommand generates a byte-identicalpublic/_headersfor exactly this reason, treating it as the app's responsibility.What changed
build.tsnow copiestemplates/public_headersto.open-next/assets/_headers. It is written after the OpenNext build, because that build regenerates the assets directory, and before the Wrangler dry-run so Wrangler validates it. The existing recursive copy into.bigcommerce/dist/assetscarries it into the uploaded bundle.#3178
a78f93cThanks @jorgemoya! - Stopcatalyst deployfrom wiping stored deployment environment variables. Commerce Hosting setup rebuilt.bigcommerce/project.jsonfrom scratch, dropping anything it didn't write itself — theenvblock managed bycatalyst env, the persistedapiHost, and stored credentials when setup ran without them. Becausedeployre-runs setup whenever the project isn't fully transformed, a routine deploy could silently discard variables that are sent as secrets on every deploy, leaving the next one to ship without them. Setup now merges into the existing file.@bigcommerce/catalyst-client@1.0.3
Patch Changes
be1967cThanks @jorgemoya! - Surface a clearer error whenBIGCOMMERCE_STOREFRONT_TOKENis not a storefront JWT. Previously an incompatible token (e.g. an OAuth access token) produced a bare 401 with no explanation. The client now detects when a 401 is returned with a token that isn't a well-formed storefront JWT and throwsInvalidStorefrontTokenErrorexplaining that a storefront JWT is required and how to generate one.@bigcommerce/create-catalyst@2.0.4
Patch Changes
9990a87,eef0c18,382bdf5,2ec54df,391f96c,ce7d1b2,5f7e630,a78f93c,d263871]:@bigcommerce/catalyst-core@1.11.0
Minor Changes
#3173
06775b2Thanks @jordanarldt! - Resolve merchant-configured locale subfolders at runtime instead of baking them in at build time, so custom locale paths such as/fr-frand/es-esresolve consistently.Previously the subfolder table was captured during
next buildintobuild-config.jsonand statically imported byi18n/locales.ts. If the control panel returned incomplete locale data at build time, every localized URL 404'd until the next deploy — and because next-intl treats a custom subfolder as a replacement for the bare locale code rather than an alias, there was no fallback:/es-essimply did not match any route.What changed
i18n/locale-config.ts, which reads locale configuration from BigCommerce at runtime and caches it in KV with the same stale-while-revalidate pattern asproxies/with-routes.ts. An empty locale list is never cached.build-config.jsonat all. A build-time snapshot of merchant-configurable data is either redundant or wrong, and using it as a fallback risked silently serving the stale URL space this change exists to fix. Runtime is now the only source. A warm cache rides out a BigCommerce outage; only a cold cache combined with an unreachable API cannot resolve, and that returns503withretry-afterrather than a 404 that would tell crawlers these pages are gone.proxies/with-intl.tsnow builds its next-intl middleware per request from that configuration. It resolves the configuration once per request and forwards it to the render asx-bc-locale-routing, so rendering and redirects reuse exactly what resolved the inbound URL rather than fetching it again — the two can't disagree, and there is no extra round trip. It also passes the matched subfolder asx-bc-locale-prefix, whichproxies/with-routes.tsstrips instead of recomputing from build-time data.Link,useRouterandusePathnamenow read the runtime configuration through a new provider inapp/[locale]/layout.tsx, so generated URLs agree with what the proxy resolves. Canonical and hreflang URLs inlib/seo/canonical.tsand the header locale switcher do the same.redirectandpermanentRedirectmoved from~/i18n/routingto~/i18n/navigation-serverand are nowasync—awaitthem. This keeps the GraphQL client and KV out of the client bundle.generateStaticParamsfromapp/[locale]/layout.tsx, which only added a build-time dependency on the locale list — every route under[locale]already renders on demand because the tree reads cookies. The build's route rendering modes are unchanged.i18n/locales.tsis removed. The locale gates ini18n/request.tsandapp/[locale]/layout.tsxnow use the runtime list, and the sitemap/robots/favicon routes resolve the default channel directly instead of routing through a locale.Behaviour change for channel-per-locale stores:
robots.txt, the sitemap index and the favicon now resolve the default channel directly rather than via the default locale, because they run outside the proxy and have no request locale. Stores that map their default locale to a non-default channel inchannels.config.tswill see those three served fromBIGCOMMERCE_CHANNEL_ID.Locale detection is unchanged: a shopper is still redirected to their language's subfolder, and an explicit choice in the locale switcher still wins on later requests.
Fixed along the way:
/xmlsitemap.phpand/adminredirected through the locale-aware helper, resolving to/<locale>/sitemap.xmland/<locale>/whenever every locale carries a prefix. The sitemap target was a 404, since/sitemap.xmlis excluded from the proxy. Both now use plain redirects;/adminalso no longer performs an uncached GraphQL request on every hit to a route that is disabled by default./<locale-code>/...instead of the configured subfolder, so alternate-locale assertions were wrong for any store whose subfolder differs from its locale code (for exampledeserved at/de-de). They now resolve the subfolder from the store.#3062
98b618fThanks @bc-yaroslav-zhmutskyi! - Add Wallet Payment buttons integration for cart pageWhat changed
getPaymentWallets,getPaymentWalletWithInitializationData, andgetCurrencyDataGraphQL queries in the cart'spage-data.tsto fetch configured wallets and their initialization data.ClientWalletButtonsclient component (core/components/wallet-buttons) that streams wallet init options and renders a container per wallet button.WalletButtonsInitializer(core/lib/wallet-buttons) that lazily injects the BigCommerce Checkout SDK loader script and initializes each wallet button against the/graphqlendpoint, with anInitializationErrorfor missing loader.walletButtonsInitOptionsandcartIdthrough the cart section component, and exposedgetCurrencyDatacurrency formatting details.with-graphql-proxy.ts/proxy.ts) to support Checkout SDK wallet-button requests.NEXT_PUBLIC_CHECKOUT_SDK_DEV_URLto.env.exampleto optionally override the Checkout SDK loader URL in development.wallet-buttons.spec.ts) verifying the loader script and wallet button containers render only when wallets are configured.Patch Changes
#3181
20b55e0Thanks @animesh1987! - Display an error message and disable Add to Cart when the requested quantity exceeds available-to-sell (on-hand + backorder allowance) on the PDP.#3181
20b55e0Thanks @animesh1987! - Only show the "ready to ship" quantity message in the cart when part of the line item is also backordered. Previously it appeared any timeshowQuantityOnHandwas enabled and any quantity was on hand, even for fully in-stock items where the message added no useful information.#3182
eb38614Thanks @parthshahp! - Fix consent-gated cookies being withheld on stores that have cookie consent disabled. c15t grants every consent category client-side when consent is disabled, but only in its in-memory store, so no consent cookie is ever written and server-side checks treated the shopper as having declined — silently dropping the selected currency and preventing thecatalyst.visitorId/catalyst.visitIdcookies from being set. Server-side consent checks now fall back to the store's cookie-consent setting when no consent cookie is present: on stores with consent disabled, consent is implicitly granted, so the analytics proxy starts visits on the first request and the currency preference persists.#3176
332aa76Thanks @jorgemoya! - Expire entries in the in-memory KV layer after 60 seconds.lib/kvkeeps a per-processMemoryKvAdapterin front of whichever shared adapter is selected (Cloudflare KV, Upstash, Vercel Runtime Cache) and skips the shared store whenever memory holds every requested key. Those entries never expired, so once a process had seen a key it stopped consulting the shared store for it entirely. Refreshes were then driven solely by theexpiryTimeeach caller embeds in the cached value — and that path refetches from the origin, not from the shared store. So every process independently refetched on a clock starting from whenever it first cached the key, rather than picking up a value another process had already fetched and shared. Cached data was never wrong, but origin requests and cache writes scaled with process count. Capacity is also raised from 500 to 4096 entries, since cache keys include the query string and so accumulate faster than the number of real paths suggests.#3171
0c49112Thanks @chanceaclark! - Fix session cookie deletion being silently broken after logout.stripSessionCookieExpirywas strippingExpiresfrom all session tokenSet-Cookieheaders, including deletion directives (empty value +Expires=past). This turned cookie deletions into permanent empty-value session cookies, so the browser never removed them and stale cookies accumulated across login/logout cycles.#3174
f647d91Thanks @jordanarldt! - Fix the product pageog:imagetag pointing at an unfetchable URL.ProductPageMetadataQueryrequestedurlTemplate, which returns a URL containing a literal{:size}placeholder that the<Image>CDN loader substitutes at render time.generateMetadatahas no such loader, so the placeholder was emitted verbatim into the Open Graph tag.Migration
In
core/app/[locale]/(default)/product/[slug]/page-data.ts, update thedefaultImageselection inProductPageMetadataQueryto request a concrete URL:defaultImage { altText - url: urlTemplate(lossy: true) + url(width: 1200, lossy: true) }width: 1200matches the Open Graph andsummary_large_imagerecommendation. Height is omitted intentionally — the stencil resizer fits the image inside the given box rather than cropping, so requesting1200x630would return a smaller square image for a square product photo.#3081
50a7263Thanks @mfaris9! - Fix Account Registration validating State/Province as required for countries without any states (e.g., Algeria). The register page now queries per-country state data from BigCommerce and hides the State/Province field entirely when the selected country has no states.#3188
96bb3c3Thanks @bc-svc-local! - Update translations.Updated dependencies [
be1967c]: