From 9a80d1d737629487b37a455ed20e5e139076a05a Mon Sep 17 00:00:00 2001 From: Matthew Volk Date: Tue, 3 Mar 2026 12:39:46 -0600 Subject: [PATCH] refactor(core): split client into base config and extended client Extract shared client configuration into a framework-agnostic `base.ts` module so `next.config.ts` no longer transitively imports `next/headers`, `next/navigation`, or `next-intl/server`. This prevents AsyncLocalStorage poisoning during config resolution in Next.js 16. --- core/client/base.ts | 31 +++++++++++++++++++++++++++++++ core/client/index.ts | 11 +++-------- core/next.config.ts | 4 ++-- 3 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 core/client/base.ts diff --git a/core/client/base.ts b/core/client/base.ts new file mode 100644 index 0000000000..0b7df95f42 --- /dev/null +++ b/core/client/base.ts @@ -0,0 +1,31 @@ +/** + * Base client configuration and instance for BigCommerce API access. + * + * This module intentionally avoids importing any Next.js runtime APIs + * (`next/headers`, `next/navigation`) or `next-intl/server`. In Next.js 16, + * importing those modules during config resolution (i.e., from `next.config.ts`) + * poisons the process-wide AsyncLocalStorage context and causes + * "workUnitAsyncStorage" invariant errors at runtime. + * + * - `baseClientConfig` — shared configuration object (env vars + logger). + * Re-used by `./index.ts` to build the full client with request hooks. + * - `baseClient` — a ready-to-use client instance without hooks. + * Safe to import from `next.config.ts` and other non-request contexts. + */ +import { createClient } from '@bigcommerce/catalyst-client'; + +import { backendUserAgent } from '../user-agent'; + +type ClientConfig = Parameters[0]; + +export const baseClientConfig = { + storefrontToken: process.env.BIGCOMMERCE_STOREFRONT_TOKEN ?? '', + storeHash: process.env.BIGCOMMERCE_STORE_HASH ?? '', + channelId: process.env.BIGCOMMERCE_CHANNEL_ID, + backendUserAgentExtensions: backendUserAgent, + logger: + (process.env.NODE_ENV !== 'production' && process.env.CLIENT_LOGGER !== 'false') || + process.env.CLIENT_LOGGER === 'true', +} satisfies Partial; + +export const baseClient = createClient(baseClientConfig); diff --git a/core/client/index.ts b/core/client/index.ts index d0b059b994..bdef1f7db2 100644 --- a/core/client/index.ts +++ b/core/client/index.ts @@ -5,7 +5,8 @@ import { redirect } from 'next/navigation'; import { getLocale as getServerLocale } from 'next-intl/server'; import { getChannelIdFromLocale } from '../channels.config'; -import { backendUserAgent } from '../user-agent'; + +import { baseClientConfig } from './base'; const getLocale = async () => { try { @@ -26,13 +27,7 @@ const getLocale = async () => { }; export const client = createClient({ - storefrontToken: process.env.BIGCOMMERCE_STOREFRONT_TOKEN ?? '', - storeHash: process.env.BIGCOMMERCE_STORE_HASH ?? '', - channelId: process.env.BIGCOMMERCE_CHANNEL_ID, - backendUserAgentExtensions: backendUserAgent, - logger: - (process.env.NODE_ENV !== 'production' && process.env.CLIENT_LOGGER !== 'false') || - process.env.CLIENT_LOGGER === 'true', + ...baseClientConfig, getChannelId: async (defaultChannelId: string) => { const locale = await getLocale(); diff --git a/core/next.config.ts b/core/next.config.ts index 1f39548d60..e77fcc0b5b 100644 --- a/core/next.config.ts +++ b/core/next.config.ts @@ -3,7 +3,7 @@ import type { NextConfig } from 'next'; import createNextIntlPlugin from 'next-intl/plugin'; import { writeBuildConfig } from './build-config/writer'; -import { client } from './client'; +import { baseClient } from './client/base'; import { graphql } from './client/graphql'; import { cspHeader } from './lib/content-security-policy'; @@ -32,7 +32,7 @@ const SettingsQuery = graphql(` `); async function writeSettingsToBuildConfig() { - const { data } = await client.fetch({ document: SettingsQuery }); + const { data } = await baseClient.fetch({ document: SettingsQuery }); const cdnEnvHostnames = process.env.NEXT_PUBLIC_BIGCOMMERCE_CDN_HOSTNAME;