Skip to content

Version Packages (canary) - #3135

Merged
jorgemoya merged 2 commits into
canaryfrom
changeset-release/canary
Aug 28, 2026
Merged

Version Packages (canary)#3135
jorgemoya merged 2 commits into
canaryfrom
changeset-release/canary

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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 9990a87 Thanks @jordanarldt! - Print the DNS records to publish when catalyst domains add succeeds. 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 382bdf5 Thanks @jordanarldt! - Standardize resource commands on plural names: catalyst project is now catalyst projects, and catalyst channel is now catalyst channels, matching the already-plural domains and logs. The singular form of every resource command remains as an alias — project, channel, domain, and log all still resolve — so existing scripts keep working. Telemetry continues to report the canonical plural name regardless of which form was typed.

  • #3192 d263871 Thanks @chanceaclark! - catalyst upgrade now 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-run changes nothing; --yes accepts.

    Two new reminders round it out: run an install when the upgrade touched your package.json, and update @bigcommerce/catalyst itself when a newer version is out.

Patch Changes

  • #3164 eef0c18 Thanks @jordanarldt! - Stop asking users to log in again after catalyst create. Credentials from the initial authentication are now written to the new project's .bigcommerce/project.json on every scaffold, not just --hosting commerce, so catalyst deploy no longer fails with "Missing credentials" and catalyst project create no longer re-prompts for login.

  • #3167 2ec54df Thanks @jordanarldt! - Print every request under catalyst 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. The request format now reads [timestamp] METHOD URL (status) [LEVEL] message, moving the level after the request details in both logs tail and logs query. catalyst logs tail --help now documents each format and notes that default and short only show requests with a message body.

  • #3193 391f96c Thanks @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: false and dynamicRoutes is empty. The next: { 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 doQueue routes revalidation through a Durable Object whose constructor reads env.WORKER_SELF_REFERENCE and throws without it:

    this.service = env.WORKER_SELF_REFERENCE;
    if (!this.service) throw new IgnorableError('No service binding for cache revalidation worker');

    That binding cannot exist on native hosting. A Cloudflare service binding 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:

    400 Bad Request  code 10143
    Service binding 'WORKER_SELF_REFERENCE' references Worker
    '…' which was not found.
    

    The dispatch_namespace binding 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 HEAD request 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. queue now uses a small self-contained queue that issues that request with a plain fetch, which leaves and re-enters through the dispatch router and arrives at the same Worker. global_fetch_strictly_public is 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_QUEUE binding 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 ce7d1b2 Thanks @jorgemoya! - Restore tag checking on regional cache hits, replacing a CDN cache purge that never worked.

    cachePurge was declared in the generated open-next.config.ts but never had credentials on native hosting, so every purge attempt no-opped with No cache zone ID or API token provided. Skipping cache purge. The declaration alone was harmful: OpenNext's isPurgeCacheEnabled() only checks whether cachePurge is declared, not whether it works. Believing purge was handling invalidation, it disabled shouldLazilyUpdateOnCacheHit — documented as on by default for 'long-lived' mode — and Catalyst additionally set bypassTagCacheOnCacheHit: 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-age window (the route's revalidate, or a 30-minute default), and revalidateTag calls 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 behind isPurgeCacheEnabled(). So tag invalidation was already durable — purge exists solely to evict incremental cache entries held in the Cache API, which is exactly the check bypassTagCacheOnCacheHit was 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

    • Removed 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."
    • Removed cachePurge: purgeCache({ type: 'durableObject' }), restoring shouldLazilyUpdateOnCacheHit to 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 revalidateTag now 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_PURGE Durable 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 5f7e630 Thanks @jorgemoya! - Ship the _headers file so hashed static assets get an immutable Cache-Control, instead of being revalidated on every repeat page view.

    packages/catalyst/templates/public_headers has always specified the right policy for /_next/static/*, but nothing ever copied it into the build output — build.ts wrote only open-next.config.ts and wrangler.jsonc, and no _headers file 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:

    cache-control: public, max-age=0, must-revalidate
    

    max-age=0, must-revalidate on 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 answered 304 Not Modified), including 4 render-blocking CSS files and 3 fonts. With the intended public,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. _headers is the supported override, and OpenNext's migrate command generates a byte-identical public/_headers for exactly this reason, treating it as the app's responsibility.

    What changed

    • build.ts now copies templates/public_headers to .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/assets carries it into the uploaded bundle.
    • Added a regression test asserting the copy happens. The original bug was not a wrong value but an absent one, with nothing asserting it — verified the new test fails when the copy is removed.
  • #3178 a78f93c Thanks @jorgemoya! - Stop catalyst deploy from wiping stored deployment environment variables. Commerce Hosting setup rebuilt .bigcommerce/project.json from scratch, dropping anything it didn't write itself — the env block managed by catalyst env, the persisted apiHost, and stored credentials when setup ran without them. Because deploy re-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

  • #3158 be1967c Thanks @jorgemoya! - Surface a clearer error when BIGCOMMERCE_STOREFRONT_TOKEN is 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 throws InvalidStorefrontTokenError explaining that a storefront JWT is required and how to generate one.

@bigcommerce/create-catalyst@2.0.4

Patch Changes

@bigcommerce/catalyst-core@1.11.0

Minor Changes

  • #3173 06775b2 Thanks @jordanarldt! - Resolve merchant-configured locale subfolders at runtime instead of baking them in at build time, so custom locale paths such as /fr-fr and /es-es resolve consistently.

    Previously the subfolder table was captured during next build into build-config.json and statically imported by i18n/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-es simply did not match any route.

    What changed

    • Added i18n/locale-config.ts, which reads locale configuration from BigCommerce at runtime and caches it in KV with the same stale-while-revalidate pattern as proxies/with-routes.ts. An empty locale list is never cached.
    • Locales are no longer written to build-config.json at 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 returns 503 with retry-after rather than a 404 that would tell crawlers these pages are gone.
    • proxies/with-intl.ts now builds its next-intl middleware per request from that configuration. It resolves the configuration once per request and forwards it to the render as x-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 as x-bc-locale-prefix, which proxies/with-routes.ts strips instead of recomputing from build-time data.
    • Link, useRouter and usePathname now read the runtime configuration through a new provider in app/[locale]/layout.tsx, so generated URLs agree with what the proxy resolves. Canonical and hreflang URLs in lib/seo/canonical.ts and the header locale switcher do the same.
    • redirect and permanentRedirect moved from ~/i18n/routing to ~/i18n/navigation-server and are now asyncawait them. This keeps the GraphQL client and KV out of the client bundle.
    • Removed generateStaticParams from app/[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.ts is removed. The locale gates in i18n/request.ts and app/[locale]/layout.tsx now 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 in channels.config.ts will see those three served from BIGCOMMERCE_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.php and /admin redirected through the locale-aware helper, resolving to /<locale>/sitemap.xml and /<locale>/ whenever every locale carries a prefix. The sitemap target was a 404, since /sitemap.xml is excluded from the proxy. Both now use plain redirects; /admin also no longer performs an uncached GraphQL request on every hit to a route that is disabled by default.
    • Losing the KV cache no longer takes the storefront down. A read failure now degrades to a fetch instead of being treated as unresolvable.
    • Locale configuration is validated where it is fetched, so an unusable value can no longer be cached and then rejected on every read, which would have silently turned the cache into a per-request refetch. Subfolders are normalized (surrounding slashes and whitespace trimmed), and locale codes and prefixes are constrained to safe URL shapes.
    • An unrecognised locale returns 404 rather than 500 when no message file exists for it.
    • A locale whose configured subfolder cannot be expressed in a URL is now skipped individually, with an error logged, instead of making the whole configuration unusable.
    • The Playwright URL fixtures asserted /<locale-code>/... instead of the configured subfolder, so alternate-locale assertions were wrong for any store whose subfolder differs from its locale code (for example de served at /de-de). They now resolve the subfolder from the store.
  • #3062 98b618f Thanks @bc-yaroslav-zhmutskyi! - Add Wallet Payment buttons integration for cart page

    What changed

    • Render wallet payment buttons (e.g. PayPal) on the cart page when payment wallets are configured for the cart.
    • Added getPaymentWallets, getPaymentWalletWithInitializationData, and getCurrencyData GraphQL queries in the cart's page-data.ts to fetch configured wallets and their initialization data.
    • Added a ClientWalletButtons client component (core/components/wallet-buttons) that streams wallet init options and renders a container per wallet button.
    • Added a WalletButtonsInitializer (core/lib/wallet-buttons) that lazily injects the BigCommerce Checkout SDK loader script and initializes each wallet button against the /graphql endpoint, with an InitializationError for missing loader.
    • Wired walletButtonsInitOptions and cartId through the cart section component, and exposed getCurrencyData currency formatting details.
    • Extended the GraphQL proxy (with-graphql-proxy.ts / proxy.ts) to support Checkout SDK wallet-button requests.
    • Added NEXT_PUBLIC_CHECKOUT_SDK_DEV_URL to .env.example to optionally override the Checkout SDK loader URL in development.
    • Added e2e coverage (wallet-buttons.spec.ts) verifying the loader script and wallet button containers render only when wallets are configured.

Patch Changes

  • #3181 20b55e0 Thanks @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 20b55e0 Thanks @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 time showQuantityOnHand was enabled and any quantity was on hand, even for fully in-stock items where the message added no useful information.

  • #3182 eb38614 Thanks @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 the catalyst.visitorId / catalyst.visitId cookies 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 332aa76 Thanks @jorgemoya! - Expire entries in the in-memory KV layer after 60 seconds. lib/kv keeps a per-process MemoryKvAdapter in 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 the expiryTime each 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 0c49112 Thanks @chanceaclark! - Fix session cookie deletion being silently broken after logout. stripSessionCookieExpiry was stripping Expires from all session token Set-Cookie headers, 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 f647d91 Thanks @jordanarldt! - Fix the product page og:image tag pointing at an unfetchable URL. ProductPageMetadataQuery requested urlTemplate, which returns a URL containing a literal {:size} placeholder that the <Image> CDN loader substitutes at render time. generateMetadata has 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 the defaultImage selection in ProductPageMetadataQuery to request a concrete URL:

      defaultImage {
        altText
    -   url: urlTemplate(lossy: true)
    +   url(width: 1200, lossy: true)
      }

    width: 1200 matches the Open Graph and summary_large_image recommendation. Height is omitted intentionally — the stencil resizer fits the image inside the given box rather than cropping, so requesting 1200x630 would return a smaller square image for a square product photo.

  • #3081 50a7263 Thanks @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 96bb3c3 Thanks @bc-svc-local! - Update translations.

  • Updated dependencies [be1967c]:

    • @bigcommerce/catalyst-client@1.0.3

@github-actions
github-actions Bot requested a review from a team as a code owner July 27, 2026 12:52
@vercel

vercel Bot commented Jul 27, 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 27, 2026 9:17pm

Request Review

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

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 93 92 94

Category Scores

Category Prod Desktop Prod Mobile Preview Desktop Preview Mobile
Performance 71 84 70 78
Accessibility 95 92 95 92
Best Practices 100 100 100 100
SEO 88 100 100 100

Core Web Vitals

Metric Prod Desktop Prod Mobile Preview Desktop Preview Mobile
LCP 5.0 s 4.5 s 4.5 s 5.9 s
CLS 0.039 0 0.039 0
FCP 1.2 s 1.2 s 1.2 s 1.2 s
TBT 0 ms 10 ms 0 ms 30 ms
Max Potential FID 50 ms 60 ms 50 ms 80 ms
Time to Interactive 5.0 s 4.5 s 4.5 s 6.0 s

Full Unlighthouse report →

@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 00ab317 to 4b49e3a Compare July 27, 2026 15:58
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 4b49e3a to 723c090 Compare July 29, 2026 14:12
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 723c090 to d1c3c19 Compare August 3, 2026 15:43
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from d1c3c19 to a55780a Compare August 4, 2026 18:49
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from a55780a to a2ac867 Compare August 5, 2026 14:44
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from a2ac867 to 3eea6a4 Compare August 5, 2026 14:53
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 3eea6a4 to 5a23986 Compare August 5, 2026 16:18
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 5a23986 to 1a31d5b Compare August 5, 2026 18:49
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 1a31d5b to a67e4d3 Compare August 5, 2026 20:42
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from a67e4d3 to 534e059 Compare August 7, 2026 11:23
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 534e059 to 0d90b1a Compare August 7, 2026 16:57
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 0d90b1a to 30f05e5 Compare August 12, 2026 15:12
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 30f05e5 to 1ed150c Compare August 18, 2026 16:10
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 689788e to a6977b7 Compare August 24, 2026 21:59
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from a6977b7 to 306c7f6 Compare August 25, 2026 18:51
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 306c7f6 to 6707c9d Compare August 26, 2026 14:55
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 6707c9d to 7e8c33b Compare August 26, 2026 17:21
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from 7e8c33b to f834d9e Compare August 26, 2026 20:57
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from f834d9e to e3fb587 Compare August 26, 2026 21:05
@github-actions
github-actions Bot force-pushed the changeset-release/canary branch from e3fb587 to c2ae56a Compare August 27, 2026 17:59
@github-actions

Copy link
Copy Markdown
Contributor Author

Bundle Size Report

Comparing against baseline from aab127a (2026-08-27).

No bundle size changes detected.

@jorgemoya
jorgemoya enabled auto-merge August 27, 2026 21:29
@jorgemoya
jorgemoya added this pull request to the merge queue Aug 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 27, 2026
@jorgemoya
jorgemoya added this pull request to the merge queue Aug 28, 2026
Merged via the queue into canary with commit 8f486d3 Aug 28, 2026
18 of 19 checks passed
@jorgemoya
jorgemoya deleted the changeset-release/canary branch August 28, 2026 00:31
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.

1 participant