From dd307ae8c11bd6c10f13cd268ec35e97dbfd716d Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Thu, 4 Dec 2025 16:44:14 +0200 Subject: [PATCH 01/15] fix(e2e): update package links in astro preset and metro config --- integration/presets/astro.ts | 2 +- integration/templates/expo-web/metro.config.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/integration/presets/astro.ts b/integration/presets/astro.ts index 65cbebff911..a45c421e23e 100644 --- a/integration/presets/astro.ts +++ b/integration/presets/astro.ts @@ -11,7 +11,7 @@ const astroNode = applicationConfig() .addScript('build', 'pnpm build') .addScript('serve', 'pnpm preview') .addDependency('@clerk/astro', linkPackage('astro', 'integration')) - .addDependency('@clerk/shared', linkPackage('types', 'integration')) + .addDependency('@clerk/shared', linkPackage('shared', 'integration')) .addDependency('@clerk/localizations', linkPackage('localizations', 'integration')); const astroStatic = astroNode.clone().setName('astro-hybrid').useTemplate(templates['astro-hybrid']); diff --git a/integration/templates/expo-web/metro.config.js b/integration/templates/expo-web/metro.config.js index 045418d1e79..c0f9eee8d78 100644 --- a/integration/templates/expo-web/metro.config.js +++ b/integration/templates/expo-web/metro.config.js @@ -51,8 +51,8 @@ if (clerkMonorepoPath) { // Explicitly map @clerk packages to their source locations // Point to the root of the package so Metro can properly resolve subpath exports config.resolver.extraNodeModules = { - '@clerk/clerk-react': path.resolve(clerkMonorepoPath, 'packages/react'), - '@clerk/clerk-expo': path.resolve(clerkMonorepoPath, 'packages/expo'), + '@clerk/react': path.resolve(clerkMonorepoPath, 'packages/react'), + '@clerk/expo': path.resolve(clerkMonorepoPath, 'packages/expo'), '@clerk/shared': path.resolve(clerkMonorepoPath, 'packages/shared'), '@clerk/types': path.resolve(clerkMonorepoPath, 'packages/types'), }; @@ -87,7 +87,7 @@ if (clerkMonorepoPath) { ]; // Custom resolver to handle package.json subpath exports for @clerk packages - // This enables Metro to resolve imports like '@clerk/clerk-react/internal' + // This enables Metro to resolve imports like '@clerk/react/internal' const originalResolveRequest = config.resolver.resolveRequest; config.resolver.resolveRequest = (context, moduleName, platform) => { // Check if this is a @clerk package with a subpath From d9ab27a40ffdb1553ab849a4b12102fd590b7186 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 13:23:25 +0200 Subject: [PATCH 02/15] refactor(ui,clerk-js): remove DevTools resource and integrate enableEnvironmentSetting into Environment resource The DevTools resource only contained a single method (__internal_enableEnvironmentSetting) which has been moved directly to the Environment resource. This simplifies the codebase by removing an unnecessary resource abstraction. Changes: - Remove DevTools resource class and file - Add __internal_enableEnvironmentSetting method to Environment resource - Update EnableOrganizationsPrompt to call environment.__internal_enableEnvironmentSetting directly - Clean up minor formatting inconsistencies in related files --- .../src/core/resources/BillingCheckout.ts | 1 - .../clerk-js/src/core/resources/DevTools.ts | 21 ------------------- .../src/core/resources/Environment.ts | 8 +++++++ packages/clerk-js/src/core/resources/index.ts | 1 - packages/clerk-js/src/core/signals.ts | 1 - packages/ui/src/Components.tsx | 3 +-- .../EnableOrganizationsPrompt/index.tsx | 2 +- .../src/customizables/AppearanceContext.tsx | 3 +-- 8 files changed, 11 insertions(+), 29 deletions(-) delete mode 100644 packages/clerk-js/src/core/resources/DevTools.ts diff --git a/packages/clerk-js/src/core/resources/BillingCheckout.ts b/packages/clerk-js/src/core/resources/BillingCheckout.ts index 0bd0736ebbd..0dbadd220fa 100644 --- a/packages/clerk-js/src/core/resources/BillingCheckout.ts +++ b/packages/clerk-js/src/core/resources/BillingCheckout.ts @@ -109,7 +109,6 @@ export const createSignals = () => { const resource = resourceSignal().resource; const error = errorSignal().error; const fetchStatus = fetchSignal().status; - const errors = errorsToParsedErrors(error, {}); return { errors: errors, fetchStatus, checkout: resource }; }, diff --git a/packages/clerk-js/src/core/resources/DevTools.ts b/packages/clerk-js/src/core/resources/DevTools.ts deleted file mode 100644 index 9a517858604..00000000000 --- a/packages/clerk-js/src/core/resources/DevTools.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ClerkResourceJSON, DevToolsResource, EnableEnvironmentSettingParams } from '@clerk/shared/types'; - -import { BaseResource } from './Base'; - -/** - * @internal - */ -export class DevTools extends BaseResource implements DevToolsResource { - pathRoot = '/dev_tools'; - - protected fromJSON(_data: ClerkResourceJSON | null): this { - return this; - } - - async __internal_enableEnvironmentSetting(params: EnableEnvironmentSettingParams) { - await this._basePatch({ - path: `${this.pathRoot}/enable_environment_setting`, - body: params, - }); - } -} diff --git a/packages/clerk-js/src/core/resources/Environment.ts b/packages/clerk-js/src/core/resources/Environment.ts index f057787a747..5e961e3f354 100644 --- a/packages/clerk-js/src/core/resources/Environment.ts +++ b/packages/clerk-js/src/core/resources/Environment.ts @@ -2,6 +2,7 @@ import type { AuthConfigResource, CommerceSettingsResource, DisplayConfigResource, + EnableEnvironmentSettingParams, EnvironmentJSON, EnvironmentJSONSnapshot, EnvironmentResource, @@ -101,4 +102,11 @@ export class Environment extends BaseResource implements EnvironmentResource { protect_config: this.protectConfig.__internal_toSnapshot(), }; } + + async __internal_enableEnvironmentSetting(params: EnableEnvironmentSettingParams) { + await this._basePatch({ + path: `/dev_tools/enable_environment_setting`, + body: params, + }); + } } diff --git a/packages/clerk-js/src/core/resources/index.ts b/packages/clerk-js/src/core/resources/index.ts index d137d5d588b..b07196edac8 100644 --- a/packages/clerk-js/src/core/resources/index.ts +++ b/packages/clerk-js/src/core/resources/index.ts @@ -1,7 +1,6 @@ export * from './AuthConfig'; export * from './Client'; export * from './DeletedObject'; -export * from './DevTools'; export * from './DisplayConfig'; export * from './EmailAddress'; export * from './Environment'; diff --git a/packages/clerk-js/src/core/signals.ts b/packages/clerk-js/src/core/signals.ts index 2046eca114c..b200154be15 100644 --- a/packages/clerk-js/src/core/signals.ts +++ b/packages/clerk-js/src/core/signals.ts @@ -62,7 +62,6 @@ export function errorsToParsedErrors>( function isFieldError(error: ClerkAPIError): boolean { return 'meta' in error && error.meta && 'paramName' in error.meta && error.meta.paramName !== undefined; } - const hasFieldErrors = error.errors.some(isFieldError); if (hasFieldErrors) { error.errors.forEach(error => { diff --git a/packages/ui/src/Components.tsx b/packages/ui/src/Components.tsx index dcf3447aeb4..719187bfadd 100644 --- a/packages/ui/src/Components.tsx +++ b/packages/ui/src/Components.tsx @@ -17,14 +17,13 @@ import type { UserProfileProps, WaitlistProps, } from '@clerk/shared/types'; - -import type { Appearance } from './internal/appearance'; import { createDeferredPromise } from '@clerk/shared/utils'; import React, { Suspense } from 'react'; import type { AppearanceCascade } from './customizables/parseAppearance'; // NOTE: Using `./hooks` instead of `./hooks/useClerkModalStateParams` will increase the bundle size import { useClerkModalStateParams } from './hooks/useClerkModalStateParams'; +import type { Appearance } from './internal/appearance'; import type { ClerkComponentName } from './lazyModules/components'; import { BlankCaptchaModal, diff --git a/packages/ui/src/components/devPrompts/EnableOrganizationsPrompt/index.tsx b/packages/ui/src/components/devPrompts/EnableOrganizationsPrompt/index.tsx index aab44a915e0..0be43788a0e 100644 --- a/packages/ui/src/components/devPrompts/EnableOrganizationsPrompt/index.tsx +++ b/packages/ui/src/components/devPrompts/EnableOrganizationsPrompt/index.tsx @@ -46,7 +46,7 @@ const EnableOrganizationsPromptInternal = ({ params.organization_allow_personal_accounts = allowPersonalAccount; } - void new DevTools() + void environment .__internal_enableEnvironmentSetting(params) .then(() => { setIsEnabled(true); diff --git a/packages/ui/src/customizables/AppearanceContext.tsx b/packages/ui/src/customizables/AppearanceContext.tsx index dfedab25078..93f3fb37be5 100644 --- a/packages/ui/src/customizables/AppearanceContext.tsx +++ b/packages/ui/src/customizables/AppearanceContext.tsx @@ -13,9 +13,8 @@ type AppearanceProviderProps = React.PropsWithChildren; const AppearanceProvider = (props: AppearanceProviderProps) => { const ctxValue = useDeepEqualMemo(() => { const value = parseAppearance(props); - return { value }; - }, [props.appearance, props.globalAppearance]); + }, [props.appearance, props.globalAppearance, props.appearanceKey]); return {props.children}; }; From bab59737605fa1412356602f2c96f86f3a569f08 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 13:23:40 +0200 Subject: [PATCH 03/15] fix(shared): reorder package exports to fix wildcard resolution in esbuild esbuild resolves package exports in order and matches the first pattern that fits. The generic "./*" wildcard was placed before the more specific "./internal/clerk-js/*" wildcard, causing esbuild to incorrectly match and fail to resolve nested paths like "@clerk/shared/internal/clerk-js/errors". This issue only manifested when packages were bundled (e.g., by Vite's dependency optimizer) rather than being symlinked from the workspace. It affected integration templates that import @clerk/ui, which in turn imports from @clerk/shared/internal/clerk-js/*. The fix moves "./internal/clerk-js/*" before "./*" so more specific patterns are matched first, following the package exports best practice. --- packages/shared/package.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/shared/package.json b/packages/shared/package.json index 82054683a52..c38c2062c9f 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -20,6 +20,16 @@ "default": "./dist/runtime/index.js" } }, + "./internal/clerk-js/*": { + "import": { + "types": "./dist/runtime/internal/clerk-js/*.d.mts", + "default": "./dist/runtime/internal/clerk-js/*.mjs" + }, + "require": { + "types": "./dist/runtime/internal/clerk-js/*.d.ts", + "default": "./dist/runtime/internal/clerk-js/*.js" + } + }, "./*": { "import": { "types": "./dist/runtime/*.d.mts", @@ -90,16 +100,6 @@ "default": "./dist/types/index.js" } }, - "./internal/clerk-js/*": { - "import": { - "types": "./dist/runtime/internal/clerk-js/*.d.mts", - "default": "./dist/runtime/internal/clerk-js/*.mjs" - }, - "require": { - "types": "./dist/runtime/internal/clerk-js/*.d.ts", - "default": "./dist/runtime/internal/clerk-js/*.js" - } - }, "./package.json": "./package.json" }, "files": [ From 8d46e77c7969397f4ab4e85d752c809ea30609b5 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 13:24:03 +0200 Subject: [PATCH 04/15] feat(shared): backport intelligent retries to loadClerkJsScript Enhanced loadClerkJsScript and loadClerkUiScript with intelligent error detection and retry logic: - Added hasScriptRequestError() function that uses Performance API to detect failed script loads by checking transferSize, decodedBodySize, responseEnd, and responseStatus - If an existing script has a request error, remove it and retry loading - If waiting for an existing script times out, remove it and allow retry - Added error event listener to bail out early on script load failures instead of waiting for full timeout - Cache scriptUrl to check for errors before attempting to wait for existing script This prevents indefinite hangs when script loads fail and enables automatic recovery from transient network issues. --- packages/shared/src/index.ts | 1 + packages/shared/src/loadClerkJsScript.ts | 101 +++++++++++++++++++---- packages/shared/src/loadScript.ts | 3 - packages/shared/src/types/environment.ts | 2 + 4 files changed, 90 insertions(+), 17 deletions(-) diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 82ff1de27e8..48634e86809 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -3,3 +3,4 @@ throw new Error( ); export {}; +// Force rebuild for explicit exports (replacing wildcard) diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index e3c096d44f5..7660f5581d9 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -55,6 +55,51 @@ function isClerkGlobalProperlyLoaded(prop: 'Clerk' | '__internal_ClerkUiCtor'): const isClerkProperlyLoaded = () => isClerkGlobalProperlyLoaded('Clerk'); const isClerkUiProperlyLoaded = () => isClerkGlobalProperlyLoaded('__internal_ClerkUiCtor'); +/** + * Checks if an existing script has a request error using Performance API. + * + * @param scriptUrl - The URL of the script to check. + * @returns True if the script has failed to load due to a network/HTTP error. + */ +function hasScriptRequestError(scriptUrl: string): boolean { + if (typeof window === 'undefined' || !window.performance) { + return false; + } + + const entries = performance.getEntriesByName(scriptUrl, 'resource') as PerformanceResourceTiming[]; + + if (entries.length === 0) { + return false; + } + + const scriptEntry = entries[entries.length - 1]; + + // transferSize === 0 with responseEnd === 0 indicates network failure + // transferSize === 0 with responseEnd > 0 might be a 4xx/5xx error or blocked request + if (scriptEntry.transferSize === 0 && scriptEntry.decodedBodySize === 0) { + // If there was no response at all, it's definitely an error + if (scriptEntry.responseEnd === 0) { + return true; + } + // If we got a response but no content, likely an HTTP error (4xx/5xx) + if (scriptEntry.responseEnd > 0 && scriptEntry.responseStart > 0) { + return true; + } + + if ('responseStatus' in scriptEntry) { + const status = (scriptEntry as any).responseStatus; + if (status >= 400) { + return true; + } + if (scriptEntry.responseStatus === 0) { + return true; + } + } + } + + return false; +} + /** * Hotloads the Clerk JS script with robust failure detection. * @@ -88,20 +133,30 @@ export const loadClerkJsScript = async (opts?: LoadClerkJsScriptOptions): Promis return null; } - const existingScript = document.querySelector('script[data-clerk-js-script]'); - - if (existingScript) { - return waitForPredicateWithTimeout(timeout, isClerkProperlyLoaded, rejectWith()); - } - if (!opts?.publishableKey) { errorThrower.throwMissingPublishableKeyError(); return null; } + const scriptUrl = clerkJsScriptUrl(opts); + const existingScript = document.querySelector('script[data-clerk-js-script]'); + + if (existingScript) { + if (hasScriptRequestError(scriptUrl)) { + existingScript.remove(); + } else { + try { + await waitForPredicateWithTimeout(timeout, isClerkProperlyLoaded, rejectWith(), existingScript); + return null; + } catch { + existingScript.remove(); + } + } + } + const loadPromise = waitForPredicateWithTimeout(timeout, isClerkProperlyLoaded, rejectWith()); - loadScript(clerkJsScriptUrl(opts), { + loadScript(scriptUrl, { async: true, crossOrigin: 'anonymous', nonce: opts.nonce, @@ -125,19 +180,30 @@ export const loadClerkUiScript = async (opts?: LoadClerkUiScriptOptions): Promis return null; } - const existingScript = document.querySelector('script[data-clerk-ui-script]'); - - if (existingScript) { - return waitForPredicateWithTimeout(timeout, isClerkUiProperlyLoaded, rejectWith()); - } - if (!opts?.publishableKey) { errorThrower.throwMissingPublishableKeyError(); return null; } + const scriptUrl = clerkUiScriptUrl(opts); + const existingScript = document.querySelector('script[data-clerk-ui-script]'); + + if (existingScript) { + if (hasScriptRequestError(scriptUrl)) { + existingScript.remove(); + } else { + try { + await waitForPredicateWithTimeout(timeout, isClerkUiProperlyLoaded, rejectWith(), existingScript); + return null; + } catch { + existingScript.remove(); + } + } + } + const loadPromise = waitForPredicateWithTimeout(timeout, isClerkUiProperlyLoaded, rejectWith()); - loadScript(clerkUiScriptUrl(opts), { + + loadScript(scriptUrl, { async: true, crossOrigin: 'anonymous', nonce: opts.nonce, @@ -223,6 +289,7 @@ function waitForPredicateWithTimeout( timeoutMs: number, predicate: () => boolean, rejectWith: Error, + existingScript?: HTMLScriptElement, ): Promise { return new Promise((resolve, reject) => { let resolved = false; @@ -232,6 +299,12 @@ function waitForPredicateWithTimeout( clearInterval(pollInterval); }; + // Bail out early if the script fails to load, instead of waiting for the entire timeout + existingScript?.addEventListener('error', () => { + cleanup(timeoutId, pollInterval); + reject(rejectWith); + }); + const checkAndResolve = () => { if (resolved) { return; diff --git a/packages/shared/src/loadScript.ts b/packages/shared/src/loadScript.ts index c9fc4a4536d..54e617ebc54 100644 --- a/packages/shared/src/loadScript.ts +++ b/packages/shared/src/loadScript.ts @@ -36,8 +36,6 @@ export async function loadScript(src = '', opts: LoadScriptOptions): Promise { - console.log('this loaded ', src); - script.remove(); resolve(script); }); @@ -56,7 +54,6 @@ export async function loadScript(src = '', opts: LoadScriptOptions): Promise { - console.log('nikos 3', _, iterations); return iterations <= 5; }, }); diff --git a/packages/shared/src/types/environment.ts b/packages/shared/src/types/environment.ts index 5a81ea75d8f..1176bc03226 100644 --- a/packages/shared/src/types/environment.ts +++ b/packages/shared/src/types/environment.ts @@ -1,6 +1,7 @@ import type { APIKeysSettingsResource } from './apiKeysSettings'; import type { AuthConfigResource } from './authConfig'; import type { CommerceSettingsResource } from './commerceSettings'; +import type { EnableEnvironmentSettingParams } from './devtools'; import type { DisplayConfigResource } from './displayConfig'; import type { OrganizationSettingsResource } from './organizationSettings'; import type { ProtectConfigResource } from './protectConfig'; @@ -23,4 +24,5 @@ export interface EnvironmentResource extends ClerkResource { maintenanceMode: boolean; clientDebugMode: boolean; __internal_toSnapshot: () => EnvironmentJSONSnapshot; + __internal_enableEnvironmentSetting: (params: EnableEnvironmentSettingParams) => Promise; } From 0e8fd11f1aa8c228a819c1cbe2e2f20c9822f003 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 13:24:28 +0200 Subject: [PATCH 05/15] fix(nuxt): support NUXT_PUBLIC_CLERK_JS_URL and NUXT_PUBLIC_CLERK_UI_URL env vars Nuxt converts camelCase runtime config keys to env var names by inserting underscores before uppercase letters. The keys clerkJSUrl/clerkUiUrl were being converted to NUXT_PUBLIC_CLERK_CLERK_J_S_URL which is not what users expect. Renamed the runtime config keys to jsUrl/uiUrl so they correctly map to NUXT_PUBLIC_CLERK_JS_URL and NUXT_PUBLIC_CLERK_UI_URL, then map them back to clerkJSUrl/clerkUiUrl when passing to the Vue plugin. This allows users to set these values via environment variables using the expected naming convention. --- packages/nuxt/src/global.d.ts | 13 ++++++++++++- packages/nuxt/src/module.ts | 6 ++++-- packages/nuxt/src/runtime/plugin.ts | 6 +++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/global.d.ts b/packages/nuxt/src/global.d.ts index 7cde099d9c9..7220a9070ab 100644 --- a/packages/nuxt/src/global.d.ts +++ b/packages/nuxt/src/global.d.ts @@ -16,7 +16,18 @@ declare module 'nuxt/schema' { }; } interface PublicRuntimeConfig { - clerk: PluginOptions; + clerk: Omit & { + /** + * The URL that `@clerk/clerk-js` should be hot-loaded from. + * Supports NUXT_PUBLIC_CLERK_JS_URL env var. + */ + jsUrl?: string; + /** + * The URL that `@clerk/ui` should be hot-loaded from. + * Supports NUXT_PUBLIC_CLERK_UI_URL env var. + */ + uiUrl?: string; + }; } } diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 33dc489a690..c5d42b4b6c3 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -64,8 +64,10 @@ export default defineNuxtModule({ signUpForceRedirectUrl: options.signUpForceRedirectUrl, signUpUrl: options.signUpUrl, domain: options.domain, - clerkJSUrl: options.clerkJSUrl, - clerkUiUrl: options.clerkUiUrl, + // Using jsUrl/uiUrl instead of clerkJSUrl/clerkUiUrl to support + // NUXT_PUBLIC_CLERK_JS_URL and NUXT_PUBLIC_CLERK_UI_URL env vars. + jsUrl: options.clerkJSUrl, + uiUrl: options.clerkUiUrl, clerkJSVariant: options.clerkJSVariant, clerkJSVersion: options.clerkJSVersion, isSatellite: options.isSatellite, diff --git a/packages/nuxt/src/runtime/plugin.ts b/packages/nuxt/src/runtime/plugin.ts index 077a2dc019b..8879860afb6 100644 --- a/packages/nuxt/src/runtime/plugin.ts +++ b/packages/nuxt/src/runtime/plugin.ts @@ -17,9 +17,13 @@ export default defineNuxtPlugin(nuxtApp => { } const runtimeConfig = useRuntimeConfig(); + const clerkConfig = runtimeConfig.public.clerk ?? {}; nuxtApp.vueApp.use(clerkPlugin as any, { - ...(runtimeConfig.public.clerk ?? {}), + ...clerkConfig, + // Map jsUrl/uiUrl to clerkJSUrl/clerkUiUrl as expected by the Vue plugin + clerkJSUrl: clerkConfig.jsUrl, + clerkUiUrl: clerkConfig.uiUrl, sdkMetadata: { name: PACKAGE_NAME, version: PACKAGE_VERSION, From 41f8ada5f3ea376724a13c4874c6ac1db7883420 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 13:25:08 +0200 Subject: [PATCH 06/15] feat(ci): use snapshot versions in verdaccio for integration tests Integration tests were failing because packages published to Verdaccio with the 'integration' dist-tag had transitive dependencies (e.g. @clerk/shared from @clerk/ui) that were resolved from npm instead of the local Verdaccio registry. Changed approach to use snapshot versions that don't exist on npm: - Create unique snapshot versions with `changeset version --snapshot ci` - Publish to 'latest' tag instead of 'integration' tag - Updated linkPackage to return '*' in CI (gets latest snapshot) - Modified snapshot.mjs to always generate changeset for all packages - Updated verdaccio action to check for snapshot versions instead of integration tag - Removed redundant long-running apps from presets that were causing test duplication This ensures all @clerk packages, including transitive dependencies, are installed from the locally published Verdaccio versions. --- .github/actions/verdaccio/action.yml | 6 +-- .github/workflows/ci.yml | 62 ++++++++++++++------------ integration/presets/longRunningApps.ts | 9 ---- integration/presets/utils.ts | 4 +- scripts/snapshot.mjs | 9 ++-- 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/.github/actions/verdaccio/action.yml b/.github/actions/verdaccio/action.yml index 13fee03e20a..6764b719d49 100644 --- a/.github/actions/verdaccio/action.yml +++ b/.github/actions/verdaccio/action.yml @@ -75,7 +75,7 @@ runs: - name: Print published Clerk package versions shell: bash run: | - echo "Published @clerk packages under 'integration' tag:" + echo "Published @clerk packages (snapshot versions):" echo "==================================================" packages=( "@clerk/agent-toolkit" @@ -102,6 +102,6 @@ runs: "@clerk/vue" ) for pkg in "${packages[@]}"; do - version=$(pnpm view "$pkg@integration" version 2>/dev/null || echo "not found") - printf "%-35s %s\n" "$pkg@integration:" "$version" + version=$(pnpm view "$pkg" version 2>/dev/null || echo "not found") + printf "%-35s %s\n" "$pkg:" "$version" done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d65f4832ed1..a084be94b70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,10 +64,10 @@ jobs: - name: Setup id: config uses: ./.github/actions/init-blacksmith - with: - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # with: + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} - name: Verify lockfile is deduped run: pnpm dedupe --check @@ -110,11 +110,11 @@ jobs: - name: Setup id: config uses: ./.github/actions/init-blacksmith - with: - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-summarize: ${{ env.TURBO_SUMMARIZE }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # with: + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-summarize: ${{ env.TURBO_SUMMARIZE }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} - name: Turbo Build run: pnpm turbo build $TURBO_ARGS --only @@ -154,11 +154,11 @@ jobs: - name: Setup id: config uses: ./.github/actions/init-blacksmith - with: - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-summarize: ${{ env.TURBO_SUMMARIZE }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # with: + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-summarize: ${{ env.TURBO_SUMMARIZE }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} - name: Check size using bundlewatch continue-on-error: true @@ -231,10 +231,10 @@ jobs: with: # Ensures that all builds are cached appropriately with a consistent run name `Unit Tests (20)`. node-version: ${{ matrix.node-version }} - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-summarize: ${{ env.TURBO_SUMMARIZE }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-summarize: ${{ env.TURBO_SUMMARIZE }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} - name: Rebuild @clerk/shared with CLERK_USE_RQ=true if: ${{ matrix.clerk-use-rq == 'true' }} @@ -280,7 +280,8 @@ jobs: retention-days: 5 integration-tests: - needs: [check-permissions, build-packages] + # needs: [check-permissions, build-packages] + needs: [check-permissions] if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} name: Integration Tests (${{ matrix.test-name }}, ${{ matrix.test-project }}${{ matrix.next-version && format(', {0}', matrix.next-version) || '' }}${{ matrix.clerk-use-rq == 'true' && ', RQ' || '' }}) permissions: @@ -355,9 +356,9 @@ jobs: id: config uses: ./.github/actions/init-blacksmith with: - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} playwright-enabled: true - name: Verify jq is installed @@ -405,12 +406,16 @@ jobs: env: CLERK_USE_RQ: true + - name: Version packages for snapshot + if: ${{ steps.task-status.outputs.affected == '1' }} + run: npm run version-packages:snapshot ci + - name: Verdaccio if: ${{ steps.task-status.outputs.affected == '1' }} uses: ./.github/actions/verdaccio with: publish-cmd: | - if [ "$(pnpm config get registry)" = "https://registry.npmjs.org/" ]; then echo 'Error: Using default registry' && exit 1; else CLERK_USE_RQ=${{ matrix.clerk-use-rq }} pnpm turbo build $TURBO_ARGS --only && pnpm changeset publish --no-git-tag --tag integration; fi + if [ "$(pnpm config get registry)" = "https://registry.npmjs.org/" ]; then echo 'Error: Using default registry' && exit 1; else ${{ matrix.clerk-use-rq == 'true' && 'CLERK_USE_RQ=true' || '' }} pnpm turbo build $TURBO_ARGS --only && pnpm changeset publish --no-git-tag --tag latest; fi - name: Edit .npmrc [link-workspace-packages=false] run: sed -i -E 's/link-workspace-packages=(deep|true)/link-workspace-packages=false/' .npmrc @@ -423,6 +428,8 @@ jobs: pnpm config set minimum-release-age-exclude @clerk/* pnpm add @clerk/backend + # Install published packages from Verdaccio to test against real npm install scenarios + # rather than local monorepo builds. Validates package structure, dependencies, and entry points. - name: Install @clerk/clerk-js in os temp if: ${{ steps.task-status.outputs.affected == '1' }} working-directory: ${{runner.temp}} @@ -483,7 +490,6 @@ jobs: E2E_CLERK_ENCRYPTION_KEY: ${{ matrix.clerk-encryption-key }} CLERK_USE_RQ: ${{ matrix.clerk-use-rq }} INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }} - MAILSAC_API_KEY: ${{ secrets.MAILSAC_API_KEY }} NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/integration/certs/rootCA.pem - name: Upload test-results @@ -518,10 +524,10 @@ jobs: with: turbo-enabled: true node-version: 22 - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-summarize: ${{ env.TURBO_SUMMARIZE }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-summarize: ${{ env.TURBO_SUMMARIZE }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} - name: Publish with pkg-pr-new run: pnpm run build && pnpx pkg-pr-new@${{ vars.PKG_PR_NEW_VERSION || '0.0.49' }} publish --compact --pnpm './packages/*' diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index a14e3c9ef83..6a324675730 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -42,15 +42,7 @@ export const createLongRunningApps = () => { * Billing apps */ { id: 'withBillingJwtV2.next.appRouter', config: next.appRouter, env: envs.withBillingJwtV2 }, - { id: 'withBilling.next.appRouter', config: next.appRouter, env: envs.withBilling }, { id: 'withBillingJwtV2.vue.vite', config: vue.vite, env: envs.withBillingJwtV2 }, - { id: 'withBilling.vue.vite', config: vue.vite, env: envs.withBilling }, - - /** - * Machine auth apps - */ - { id: 'withMachine.express.vite', config: express.vite, env: envs.withAPIKeys }, - { id: 'withMachine.next.appRouter', config: next.appRouter, env: envs.withAPIKeys }, /** * Vite apps - basic flows @@ -68,7 +60,6 @@ export const createLongRunningApps = () => { /** * Various apps - basic flows */ - { id: 'withBilling.astro.node', config: astro.node, env: envs.withBilling }, { id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles }, { id: 'astro.static.withCustomRoles', config: astro.static, env: envs.withCustomRoles }, { id: 'expo.expo-web', config: expo.expoWeb, env: envs.withEmailCodes }, diff --git a/integration/presets/utils.ts b/integration/presets/utils.ts index d22e39250dd..21672d16b5b 100644 --- a/integration/presets/utils.ts +++ b/integration/presets/utils.ts @@ -3,7 +3,9 @@ import path from 'node:path'; export function linkPackage(pkg: string, tag?: string) { // eslint-disable-next-line turbo/no-undeclared-env-vars if (process.env.CI === 'true') { - return tag || '*'; + // In CI, use '*' to get the latest version from Verdaccio + // which will be the snapshot version we just published + return '*'; } return `link:${path.resolve(process.cwd(), `packages/${pkg}`)}`; diff --git a/scripts/snapshot.mjs b/scripts/snapshot.mjs index 138c77b7ea3..57eec8287c2 100755 --- a/scripts/snapshot.mjs +++ b/scripts/snapshot.mjs @@ -25,13 +25,14 @@ try { // this will remove the prerelease versions // but will also clear the changeset .md files await $`pnpm changeset version`; - // generate a temp .md file that indicates that all packages have changes - // in order to force a snapshot release - await $`touch .changeset/snap.md && echo ${snapshot} > .changeset/snap.md`; } catch { - // otherwise, do nothing + // not in pre-release mode, continue } +// Always generate a temp .md file that indicates that all packages have changes +// in order to force a snapshot release of all packages +await $`touch .changeset/snap.md && echo ${snapshot} > .changeset/snap.md`; + const res = await $`pnpm changeset version --snapshot ${prefix}`; const success = !res.stderr.includes('No unreleased changesets found'); From 03053cb42ce49d82b61a1b9458666469ebd98dbe Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 13:26:10 +0200 Subject: [PATCH 07/15] refactor(turbo): remove redundant build dependencies from integration tests Integration tests now install packages from Verdaccio with snapshot versions, so they don't need to depend on local builds. Removing these dependencies simplifies the turbo pipeline and allows integration tests to run without waiting for local package builds to complete. Removed dependsOn arrays from all test:integration:* tasks including: - nextjs, nextjs:canary, quickstart - astro, express, ap-flows, generic - localhost, react, remix, tanstack, vue, nuxt, expo Tests will still have access to properly built packages via the snapshot versions published to Verdaccio in the CI pipeline. --- .github/workflows/nightly-checks.yml | 5 +- turbo.json | 139 --------------------------- 2 files changed, 2 insertions(+), 142 deletions(-) diff --git a/.github/workflows/nightly-checks.yml b/.github/workflows/nightly-checks.yml index f74f87a2e0e..79ea94e5fdf 100644 --- a/.github/workflows/nightly-checks.yml +++ b/.github/workflows/nightly-checks.yml @@ -68,10 +68,9 @@ jobs: E2E_CLERK_UI_VERSION: "latest" E2E_NEXTJS_VERSION: "canary" E2E_NPM_FORCE: "true" - E2E_REACT_DOM_VERSION: "19.1.0" - E2E_REACT_VERSION: "19.1.0" + E2E_REACT_DOM_VERSION: "19.2.1" + E2E_REACT_VERSION: "19.2.1" INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }} - MAILSAC_API_KEY: ${{ secrets.MAILSAC_API_KEY }} # Upload test artifacts if tests failed - name: Upload Test Artifacts diff --git a/turbo.json b/turbo.json index f3dfbc66ba8..8ef7b479afc 100644 --- a/turbo.json +++ b/turbo.json @@ -193,240 +193,101 @@ "outputs": [] }, "//#test:integration:ap-flows": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/nextjs#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:generic": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/react#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:express": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/express#build", - "@clerk/react#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:nextjs": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/nextjs#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:nextjs:canary": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/nextjs#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:quickstart": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/nextjs#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:astro": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/astro#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:localhost": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/nextjs#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "NODE_EXTRA_CA_CERTS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:sessions": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/nextjs#build" - ], "env": ["CLEANUP", "DEBUG", "DISABLE_WEB_SECURITY", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "NODE_EXTRA_CA_CERTS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:sessions:staging": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/nextjs#build" - ], "env": ["CLEANUP", "DEBUG", "DISABLE_WEB_SECURITY", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "NODE_EXTRA_CA_CERTS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:handshake": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/nextjs#build" - ], "env": ["CLEANUP", "DEBUG", "DISABLE_WEB_SECURITY", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "NODE_EXTRA_CA_CERTS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:handshake:staging": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/nextjs#build" - ], "env": ["CLEANUP", "DEBUG", "DISABLE_WEB_SECURITY", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "NODE_EXTRA_CA_CERTS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:expo-web": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/expo#build", - "@clerk/react#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:tanstack-react-start": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/tanstack-react-start#build", - "@clerk/react#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:vue": { - "dependsOn": ["@clerk/testing#build", "@clerk/clerk-js#build", "@clerk/ui#build", "@clerk/vue#build"], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:nuxt": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/vue#build", - "@clerk/backend#build", - "@clerk/nuxt#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:react-router": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/react-router#build", - "@clerk/backend#build", - "@clerk/react#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:billing": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/nextjs#build", - "@clerk/vue#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:machine": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/nextjs#build", - "@clerk/express#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" }, "//#test:integration:custom": { - "dependsOn": [ - "@clerk/testing#build", - "@clerk/clerk-js#build", - "@clerk/ui#build", - "@clerk/backend#build", - "@clerk/react#build" - ], "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" From 1e231d824ab707fd2bf85e55946ef50735f914d4 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 13:26:36 +0200 Subject: [PATCH 08/15] feat(e2e): automatically setup clerk testing tokens in app.dev() Integration tests that create their own applications (using `.clone().commit()`) were failing in CI with "CLERK_FAPI is required" errors. This happened because `setupClerkTestingToken()` needs the CLERK_FAPI environment variable, which is set by calling `clerkSetup()`. Long-running apps automatically call `clerkSetup()` during init, but apps created inline in tests did not. Rather than requiring each test to manually call `clerkSetup()`, moved it into the `Application.dev()` method to run automatically after the server starts. This ensures all applications (both long-running and inline) have proper testing token setup without manual intervention, preventing future test failures and reducing boilerplate. Additionally, added logging of all installed @clerk/* packages (including transitive dependencies) during setup to aid in debugging version mismatches in CI. --- integration/models/application.ts | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/integration/models/application.ts b/integration/models/application.ts index c8a6b2df2aa..c4b2b059a92 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -1,5 +1,8 @@ import * as path from 'node:path'; +import { parsePublishableKey } from '@clerk/shared/keys'; +import { clerkSetup } from '@clerk/testing/playwright'; + import { awaitableTreekill, createLogger, fs, getPort, run, waitForIdleProcess, waitForServer } from '../scripts'; import type { ApplicationConfig } from './applicationConfig.js'; import type { EnvironmentConfig } from './environment.js'; @@ -46,6 +49,10 @@ export const application = ( const log = logger.child({ prefix: 'setup' }).info; await run(scripts.setup, { cwd: appDirPath, log }); state.completedSetup = true; + // Print all Clerk package versions (direct and transitive) + const clerkPackagesLog = logger.child({ prefix: 'clerk-packages' }).info; + clerkPackagesLog('Installed @clerk/* packages:'); + await run('pnpm list @clerk/* --depth 100', { cwd: appDirPath, log: clerkPackagesLog }); } }, dev: async (opts: { port?: number; manualStart?: boolean; detached?: boolean; serverUrl?: string } = {}) => { @@ -82,6 +89,36 @@ export const application = ( log(`Server started at ${runtimeServerUrl}, pid: ${proc.pid}`); cleanupFns.push(() => awaitableTreekill(proc.pid, 'SIGKILL')); state.serverUrl = runtimeServerUrl; + + // Setup Clerk testing tokens after the server is running + if (state.env) { + try { + const publishableKey = state.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'); + const secretKey = state.env.privateVariables.get('CLERK_SECRET_KEY'); + const apiUrl = state.env.privateVariables.get('CLERK_API_URL'); + + if (publishableKey && secretKey) { + const { instanceType, frontendApi: frontendApiUrl } = parsePublishableKey(publishableKey); + + if (instanceType !== 'development') { + log('Skipping clerkSetup for non-development instance'); + } else { + await clerkSetup({ + publishableKey, + frontendApiUrl, + secretKey, + // @ts-expect-error apiUrl is not a typed option for clerkSetup, but it is accepted at runtime. + apiUrl, + dotenv: false, + }); + log('Clerk testing tokens setup complete'); + } + } + } catch (error) { + logger.warn('Failed to setup Clerk testing tokens:', error); + } + } + return { port, serverUrl: runtimeServerUrl, pid: proc.pid }; }, build: async () => { From 3e3511ab14dbc1cfefa0eb0be3f27e11ec99edf3 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 13:26:56 +0200 Subject: [PATCH 09/15] chore(e2e): drop mailsac and improve error logging in test utilities Removed Mailsac API integration from emailService as it's no longer needed for integration tests. Tests now use fictional emails with +clerk_test suffix that can be verified using the 424242 OTP without sending actual emails. Added withErrorLogging wrapper function to usersService that logs detailed error information (status, message, errors, clerkTraceId) for all Clerk API calls. This improves debugging when tests fail by providing immediate visibility into API errors without requiring log aggregation. Wrapped all clerkClient calls in usersService with withErrorLogging including: - User operations: createUser, getUser, getUserList, deleteUser - Organization operations: createOrganization, deleteOrganization - API key operations: create, revoke --- .../templates/astro-hybrid/astro.config.mjs | 11 ++- .../templates/astro-node/astro.config.mjs | 12 ++- .../custom-flows-react-vite/src/main.tsx | 5 ++ .../templates/expo-web/app/_layout.tsx | 5 ++ .../src/app/layout.tsx | 8 +- .../(tests)/has-ssr/page.tsx | 9 +- .../next-app-router/src/app/layout.tsx | 3 +- .../src/app/settings/useAuth-has/layout.tsx | 13 ++- .../templates/nuxt-node/nuxt.config.js | 7 ++ integration/templates/react-cra/src/index.tsx | 5 ++ .../react-router-library/src/main.tsx | 9 +- .../templates/react-router-node/app/root.tsx | 9 +- integration/templates/react-vite/src/main.tsx | 5 ++ .../src/routes/__root.tsx | 8 +- integration/templates/vue-vite/src/main.ts | 5 ++ integration/testUtils/emailService.ts | 2 - integration/testUtils/usersService.ts | 83 ++++++++++++------- 17 files changed, 160 insertions(+), 39 deletions(-) diff --git a/integration/templates/astro-hybrid/astro.config.mjs b/integration/templates/astro-hybrid/astro.config.mjs index 30ff739e8a3..8568979754d 100644 --- a/integration/templates/astro-hybrid/astro.config.mjs +++ b/integration/templates/astro-hybrid/astro.config.mjs @@ -4,7 +4,16 @@ import react from '@astrojs/react'; export default defineConfig({ output: 'hybrid', - integrations: [clerk(), react()], + integrations: [ + clerk({ + appearance: { + options: { + showOptionalFields: true, + }, + }, + }), + react(), + ], server: { port: Number(process.env.PORT), }, diff --git a/integration/templates/astro-node/astro.config.mjs b/integration/templates/astro-node/astro.config.mjs index 6b08d1babd3..41e54f926f8 100644 --- a/integration/templates/astro-node/astro.config.mjs +++ b/integration/templates/astro-node/astro.config.mjs @@ -10,7 +10,17 @@ export default defineConfig({ adapter: node({ mode: 'standalone', }), - integrations: [clerk(), react(), tailwind()], + integrations: [ + clerk({ + appearance: { + options: { + showOptionalFields: true, + }, + }, + }), + react(), + tailwind(), + ], server: { port: Number(process.env.PORT), }, diff --git a/integration/templates/custom-flows-react-vite/src/main.tsx b/integration/templates/custom-flows-react-vite/src/main.tsx index 966d034a194..f89e557a8dc 100644 --- a/integration/templates/custom-flows-react-vite/src/main.tsx +++ b/integration/templates/custom-flows-react-vite/src/main.tsx @@ -23,6 +23,11 @@ createRoot(document.getElementById('root')!).render( publishableKey={PUBLISHABLE_KEY} clerkJSUrl={import.meta.env.VITE_CLERK_JS_URL as string} clerkUiUrl={import.meta.env.VITE_CLERK_UI_URL as string} + appearance={{ + options: { + showOptionalFields: true, + }, + }} > diff --git a/integration/templates/expo-web/app/_layout.tsx b/integration/templates/expo-web/app/_layout.tsx index 24295ee033d..60c238a6892 100644 --- a/integration/templates/expo-web/app/_layout.tsx +++ b/integration/templates/expo-web/app/_layout.tsx @@ -10,6 +10,11 @@ export default function RootLayout() { routerReplace={to => router.replace(to)} clerkJSUrl={process.env.EXPO_PUBLIC_CLERK_JS_URL} clerkUiUrl={process.env.EXPO_PUBLIC_CLERK_UI_URL} + appearance={{ + options: { + showOptionalFields: true, + }, + }} > diff --git a/integration/templates/next-app-router-quickstart/src/app/layout.tsx b/integration/templates/next-app-router-quickstart/src/app/layout.tsx index 29ddd566bdb..411ba883c93 100644 --- a/integration/templates/next-app-router-quickstart/src/app/layout.tsx +++ b/integration/templates/next-app-router-quickstart/src/app/layout.tsx @@ -11,7 +11,13 @@ export const metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + {children} diff --git a/integration/templates/next-app-router/src/app/jwt-v2-organizations/(tests)/has-ssr/page.tsx b/integration/templates/next-app-router/src/app/jwt-v2-organizations/(tests)/has-ssr/page.tsx index a7d7102bb50..9c937cc10fb 100644 --- a/integration/templates/next-app-router/src/app/jwt-v2-organizations/(tests)/has-ssr/page.tsx +++ b/integration/templates/next-app-router/src/app/jwt-v2-organizations/(tests)/has-ssr/page.tsx @@ -3,7 +3,14 @@ import { SSR } from './client'; export default function Page() { return ( - + ); diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx index 2e56184f39d..42341d04adc 100644 --- a/integration/templates/next-app-router/src/app/layout.tsx +++ b/integration/templates/next-app-router/src/app/layout.tsx @@ -13,7 +13,8 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( {children}; + return ( + + {children} + + ); } diff --git a/integration/templates/nuxt-node/nuxt.config.js b/integration/templates/nuxt-node/nuxt.config.js index f60e469817f..68df62e2b2b 100644 --- a/integration/templates/nuxt-node/nuxt.config.js +++ b/integration/templates/nuxt-node/nuxt.config.js @@ -1,5 +1,12 @@ export default defineNuxtConfig({ modules: ['@clerk/nuxt'], + clerk: { + appearance: { + options: { + showOptionalFields: true, + }, + }, + }, devtools: { enabled: false, }, diff --git a/integration/templates/react-cra/src/index.tsx b/integration/templates/react-cra/src/index.tsx index 5a20abd8759..65271a25b7e 100644 --- a/integration/templates/react-cra/src/index.tsx +++ b/integration/templates/react-cra/src/index.tsx @@ -11,6 +11,11 @@ root.render( publishableKey={process.env.REACT_APP_CLERK_PUBLISHABLE_KEY as string} clerkJSUrl={process.env.REACT_APP_CLERK_JS as string} clerkUiUrl={process.env.REACT_APP_CLERK_UI as string} + appearance={{ + options: { + showOptionalFields: true, + }, + }} > diff --git a/integration/templates/react-router-library/src/main.tsx b/integration/templates/react-router-library/src/main.tsx index 46ab36679fd..8c1fd62ded1 100644 --- a/integration/templates/react-router-library/src/main.tsx +++ b/integration/templates/react-router-library/src/main.tsx @@ -10,7 +10,14 @@ const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; createRoot(document.getElementById('root')!).render( - + +
diff --git a/integration/templates/react-vite/src/main.tsx b/integration/templates/react-vite/src/main.tsx index f35d944287b..7f60469deeb 100644 --- a/integration/templates/react-vite/src/main.tsx +++ b/integration/templates/react-vite/src/main.tsx @@ -34,6 +34,11 @@ const Root = () => { clerkUiUrl={import.meta.env.VITE_CLERK_UI_URL as string} routerPush={(to: string) => navigate(to)} routerReplace={(to: string) => navigate(to, { replace: true })} + appearance={{ + options: { + showOptionalFields: true, + }, + }} experimental={{ persistClient: import.meta.env.VITE_EXPERIMENTAL_PERSIST_CLIENT ? import.meta.env.VITE_EXPERIMENTAL_PERSIST_CLIENT === 'true' diff --git a/integration/templates/tanstack-react-start/src/routes/__root.tsx b/integration/templates/tanstack-react-start/src/routes/__root.tsx index ecf10d8fcc2..fc3ad1576b0 100644 --- a/integration/templates/tanstack-react-start/src/routes/__root.tsx +++ b/integration/templates/tanstack-react-start/src/routes/__root.tsx @@ -22,7 +22,13 @@ function RootComponent() { function RootDocument({ children }: { children: React.ReactNode }) { return ( - + diff --git a/integration/templates/vue-vite/src/main.ts b/integration/templates/vue-vite/src/main.ts index d8980dd44a8..f3c1a9e4638 100644 --- a/integration/templates/vue-vite/src/main.ts +++ b/integration/templates/vue-vite/src/main.ts @@ -10,6 +10,11 @@ app.use(clerkPlugin, { clerkJSUrl: import.meta.env.VITE_CLERK_JS_URL, clerkUiUrl: import.meta.env.VITE_CLERK_UI_URL, clerkJSVersion: import.meta.env.VITE_CLERK_JS_VERSION, + appearance: { + options: { + showOptionalFields: true, + }, + }, }); app.use(router); app.mount('#app'); diff --git a/integration/testUtils/emailService.ts b/integration/testUtils/emailService.ts index 0de10de747b..c1cb085494d 100644 --- a/integration/testUtils/emailService.ts +++ b/integration/testUtils/emailService.ts @@ -12,8 +12,6 @@ export const createEmailService = () => { const fetcher = async (url: string | URL, init?: RequestInit) => { const headers = new Headers(init?.headers || {}); - // eslint-disable-next-line turbo/no-undeclared-env-vars - headers.set('Mailsac-Key', process.env.MAILSAC_API_KEY); return fetch(url, { ...init, headers }); }; diff --git a/integration/testUtils/usersService.ts b/integration/testUtils/usersService.ts index 52814990c92..eecab30dbf4 100644 --- a/integration/testUtils/usersService.ts +++ b/integration/testUtils/usersService.ts @@ -3,6 +3,19 @@ import { faker } from '@faker-js/faker'; import { hash } from '../models/helpers'; +async function withErrorLogging(operation: string, fn: () => Promise): Promise { + try { + return await fn(); + } catch (e: any) { + console.error(`[usersService] ${operation} failed:`); + console.error(' Status:', e.status); + console.error(' Message:', e.message); + console.error(' Errors:', JSON.stringify(e.errors, null, 2)); + console.error(' Clerk Trace ID:', e.clerkTraceId); + throw e; + } +} + type FakeUserOptions = { /** * A fictional email is an email that contains +clerk_test and can always be verified using 424242 as the OTP. No email will be sent. @@ -126,15 +139,17 @@ export const createUserService = (clerkClient: ClerkClient) => { }; }, createBapiUser: async fakeUser => { - return await clerkClient.users.createUser({ - emailAddress: fakeUser.email !== undefined ? [fakeUser.email] : undefined, - password: fakeUser.password, - firstName: fakeUser.firstName, - lastName: fakeUser.lastName, - phoneNumber: fakeUser.phoneNumber !== undefined ? [fakeUser.phoneNumber] : undefined, - username: fakeUser.username, - skipPasswordRequirement: fakeUser.password === undefined, - }); + return withErrorLogging('createBapiUser', () => + clerkClient.users.createUser({ + emailAddress: fakeUser.email !== undefined ? [fakeUser.email] : undefined, + password: fakeUser.password, + firstName: fakeUser.firstName, + lastName: fakeUser.lastName, + phoneNumber: fakeUser.phoneNumber !== undefined ? [fakeUser.phoneNumber] : undefined, + username: fakeUser.username, + skipPasswordRequirement: fakeUser.password === undefined, + }), + ); }, getOrCreateUser: async fakeUser => { const existingUser = await self.getUser({ email: fakeUser.email }); @@ -147,10 +162,12 @@ export const createUserService = (clerkClient: ClerkClient) => { let id = opts.id; if (!id) { - const { data: users } = await clerkClient.users.getUserList({ - emailAddress: [opts.email], - phoneNumber: [opts.phoneNumber], - }); + const { data: users } = await withErrorLogging('getUserList', () => + clerkClient.users.getUserList({ + emailAddress: [opts.email], + phoneNumber: [opts.phoneNumber], + }), + ); id = users[0]?.id; } @@ -159,12 +176,12 @@ export const createUserService = (clerkClient: ClerkClient) => { return; } - await clerkClient.users.deleteUser(id); + await withErrorLogging('deleteUser', () => clerkClient.users.deleteUser(id)); }, getUser: async (opts: { id?: string; email?: string }) => { if (opts.id) { try { - const user = await clerkClient.users.getUser(opts.id); + const user = await withErrorLogging('getUser', () => clerkClient.users.getUser(opts.id)); return user; } catch (err) { console.log(`Error fetching user "${opts.id}": ${err.message}`); @@ -173,7 +190,9 @@ export const createUserService = (clerkClient: ClerkClient) => { } if (opts.email) { - const { data: users } = await clerkClient.users.getUserList({ emailAddress: [opts.email] }); + const { data: users } = await withErrorLogging('getUserList', () => + clerkClient.users.getUserList({ emailAddress: [opts.email] }), + ); if (users.length > 0) { return users[0]; } else { @@ -186,33 +205,41 @@ export const createUserService = (clerkClient: ClerkClient) => { }, createFakeOrganization: async userId => { const name = faker.animal.dog(); - const organization = await clerkClient.organizations.createOrganization({ - name: faker.animal.dog(), - createdBy: userId, - }); + const organization = await withErrorLogging('createOrganization', () => + clerkClient.organizations.createOrganization({ + name: faker.animal.dog(), + createdBy: userId, + }), + ); return { name, organization, - delete: () => clerkClient.organizations.deleteOrganization(organization.id), + delete: () => + withErrorLogging('deleteOrganization', () => clerkClient.organizations.deleteOrganization(organization.id)), } satisfies FakeOrganization; }, createFakeAPIKey: async (userId: string) => { const TWENTY_MINUTES = 20 * 60; - const apiKey = await clerkClient.apiKeys.create({ - subject: userId, - name: `Integration Test - ${faker.string.uuid()}`, - secondsUntilExpiration: TWENTY_MINUTES, - }); + const apiKey = await withErrorLogging('createAPIKey', () => + clerkClient.apiKeys.create({ + subject: userId, + name: `Integration Test - ${faker.string.uuid()}`, + secondsUntilExpiration: TWENTY_MINUTES, + }), + ); return { apiKey, secret: apiKey.secret ?? '', - revoke: () => clerkClient.apiKeys.revoke({ apiKeyId: apiKey.id, revocationReason: 'For testing purposes' }), + revoke: () => + withErrorLogging('revokeAPIKey', () => + clerkClient.apiKeys.revoke({ apiKeyId: apiKey.id, revocationReason: 'For testing purposes' }), + ), } satisfies FakeAPIKey; }, passwordCompromised: async (userId: string) => { - await clerkClient.users.__experimental_passwordCompromised(userId); + await withErrorLogging('passwordCompromised', () => clerkClient.users.__experimental_passwordCompromised(userId)); }, }; From 0155ad60dee39611a8d9d70c433393e1bdb14a46 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 13:28:02 +0200 Subject: [PATCH 10/15] fix(e2e): simplify billing tests and fix environment configuration Simplified billing-related integration tests by removing explicit environment constraints. Tests now run against the default withBilling environment instead of requiring explicit withEnv configuration, reducing test setup complexity. Fixed withBillingJwtV2 environment configuration: - Removed staging API URL override (now uses default production API) - Changed from 'with-billing-staging' to 'with-billing' instance keys - This aligns the test environment with the production billing setup Added CLERK_JS_URL and CLERK_UI_URL to withCustomRoles environment to support local development and CI testing scenarios where custom Clerk script URLs are needed. Also fixed minor typo in test name and removed extraneous blank line. --- integration/presets/envs.ts | 9 +++++---- integration/tests/billing-hooks.test.ts | 3 +-- integration/tests/pricing-table.test.ts | 6 ++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 6135840636f..43245f0093e 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -65,7 +65,9 @@ const withCustomRoles = base .clone() .setId('withCustomRoles') .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-custom-roles').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-custom-roles').pk); + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-custom-roles').pk) + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('public', 'CLERK_UI_URL', constants.E2E_APP_CLERK_UI || 'http://localhost:18212/ui.browser.js'); const withReverification = base .clone() @@ -165,9 +167,8 @@ const withSessionTasksResetPassword = base const withBillingJwtV2 = base .clone() .setId('withBillingJwtV2') - .setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing-staging').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing-staging').pk); + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing').sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing').pk); const withBilling = base .clone() diff --git a/integration/tests/billing-hooks.test.ts b/integration/tests/billing-hooks.test.ts index 48c441e6104..daa474f52a4 100644 --- a/integration/tests/billing-hooks.test.ts +++ b/integration/tests/billing-hooks.test.ts @@ -1,10 +1,9 @@ import { expect, test } from '@playwright/test'; -import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; -testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('billing hooks @billing', ({ app }) => { +testAgainstRunningApps({})('billing hooks @billing', ({ app }) => { test.describe.configure({ mode: 'parallel' }); test.skip(!app.name.includes('next'), 'Skipping: Only runs on next'); diff --git a/integration/tests/pricing-table.test.ts b/integration/tests/pricing-table.test.ts index bdc61418b6c..f54917c6f21 100644 --- a/integration/tests/pricing-table.test.ts +++ b/integration/tests/pricing-table.test.ts @@ -1,11 +1,10 @@ import type { Locator } from '@playwright/test'; import { expect, test } from '@playwright/test'; -import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; -testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing table @billing', ({ app }) => { +testAgainstRunningApps({})('pricing table @billing', ({ app }) => { test.describe.configure({ mode: 'parallel' }); let fakeUser: FakeUser; @@ -639,7 +638,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl await fakeUser.deleteIfExists(); }); - test('displays notice then plan cannot change', async ({ page, context }) => { + test('displays notice the plan cannot change', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); const fakeUser = u.services.users.createFakeUser(); @@ -657,7 +656,6 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl await u.po.checkout.fillTestCard(); await u.po.checkout.clickPayOrSubscribe(); await expect(u.po.page.getByText('Payment was successful!')).toBeVisible(); - await u.po.checkout.confirmAndContinue(); await u.po.pricingTable.startCheckout({ planSlug: 'pro', shouldSwitch: true, period: 'monthly' }); await u.po.checkout.waitForMounted(); From e3cdf23f0cf4428ce47a9bc84e896dc77151c6a3 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 13:28:58 +0200 Subject: [PATCH 11/15] chore(e2e): update integration presets and simplify test configuration Simplified integration test configuration and removed redundant setup: - Removed 'integration' dist-tag from linkPackage calls in all presets (astro, express, react, custom-flows) since tests now use snapshot versions from Verdaccio - Renamed CLEANUP to E2E_CLEANUP in constants for better namespacing with other E2E environment variables - Removed manual clerkSetup() calls from custom-flows tests since this is now handled automatically in Application.dev() - Converted machine-auth component test from testAgainstRunningApps pattern to inline app creation, providing more flexibility and reducing long-running app dependencies - Removed MAILSAC_API_KEY from .env.local.sample as mailsac is no longer used - Updated README.md to reflect these changes These changes reduce boilerplate and test complexity while maintaining the same test coverage. --- integration/.env.local.sample | 1 - integration/README.md | 5 ++--- integration/constants.ts | 4 ++-- integration/presets/astro.ts | 6 +++--- integration/presets/custom-flows.ts | 6 +++--- integration/presets/express.ts | 6 +++--- integration/presets/react.ts | 6 +++--- integration/tests/custom-flows/sign-in.test.ts | 16 ---------------- integration/tests/custom-flows/sign-up.test.ts | 16 ---------------- integration/tests/global.teardown.ts | 2 +- integration/tests/machine-auth/component.test.ts | 16 +++++++++++----- 11 files changed, 28 insertions(+), 56 deletions(-) diff --git a/integration/.env.local.sample b/integration/.env.local.sample index 9505f3baa4c..8fdb1f0151c 100644 --- a/integration/.env.local.sample +++ b/integration/.env.local.sample @@ -1,4 +1,3 @@ -MAILSAC_API_KEY= VERCEL_PROJECT_ID= VERCEL_ORG_ID= VERCEL_TOKEN= diff --git a/integration/README.md b/integration/README.md index 64e26f9ac06..ff565b2cce5 100644 --- a/integration/README.md +++ b/integration/README.md @@ -75,10 +75,10 @@ Below you can find code snippets for running tests in a specific manner, easily #### Keep temporary site -During E2E runs a temporary site is created in which the template is copied into. If you want to keep the site around, pass the `CLEANUP` environment variable: +During E2E runs a temporary site is created in which the template is copied into. If you want to keep the site around, pass the `E2E_CLEANUP` environment variable: ```shell -CLEANUP=0 pnpm test:integration:base +E2E_CLEANUP=0 pnpm test:integration:base ``` For all available environment variables, check the [`constants.ts`](../integration/constants.ts) file. @@ -578,7 +578,6 @@ Before writing tests, it's important to understand how Playwright handles test i > [!NOTE] > The test suite also uses these environment variables to run some tests: > -> - `MAILSAC_API_KEY`: Used for [Mailsac](https://mailsac.com/) to retrieve email codes and magic links from temporary email addresses. > - `VERCEL_PROJECT_ID`: Only required if you plan on running deployment tests locally. This is the Vercel project ID, and it points to an application created via the Vercel dashboard. The easiest way to get access to it is by linking a local app to the Vercel project using the Vercel CLI, and then copying the values from the `.vercel` directory. > - `VERCEL_ORG_ID`: The organization that owns the Vercel project. See above for more details. > - `VERCEL_TOKEN`: A personal access token. This corresponds to a real user running the deployment command. Attention: Be extra careful with this token as it can't be scoped to a single Vercel project, meaning that the token has access to every project in the account it belongs to. diff --git a/integration/constants.ts b/integration/constants.ts index 7195880dba2..227d6e267c3 100644 --- a/integration/constants.ts +++ b/integration/constants.ts @@ -38,10 +38,10 @@ export const constants = { */ E2E_APP_CLERK_UI_DIR: process.env.E2E_APP_CLERK_UI_DIR, /** - * If CLEANUP=0 is used, the .tmp_integration directory will not be deleted. + * If E2E_CLEANUP=0 is used, the .tmp_integration directory will not be deleted. * This is useful for debugging locally. */ - CLEANUP: !(process.env.CLEANUP === '0' || process.env.CLEANUP === 'false'), + E2E_CLEANUP: !(process.env.E2E_CLEANUP === '0' || process.env.E2E_CLEANUP === 'false'), DEBUG: process.env.DEBUG === 'true' || process.env.DEBUG === '1', /** * Used with E2E_APP_URL if the tests need to access BAPI. diff --git a/integration/presets/astro.ts b/integration/presets/astro.ts index a45c421e23e..e9e310bf5fc 100644 --- a/integration/presets/astro.ts +++ b/integration/presets/astro.ts @@ -10,9 +10,9 @@ const astroNode = applicationConfig() .addScript('dev', 'pnpm dev') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm preview') - .addDependency('@clerk/astro', linkPackage('astro', 'integration')) - .addDependency('@clerk/shared', linkPackage('shared', 'integration')) - .addDependency('@clerk/localizations', linkPackage('localizations', 'integration')); + .addDependency('@clerk/astro', linkPackage('astro')) + .addDependency('@clerk/shared', linkPackage('shared')) + .addDependency('@clerk/localizations', linkPackage('localizations')); const astroStatic = astroNode.clone().setName('astro-hybrid').useTemplate(templates['astro-hybrid']); diff --git a/integration/presets/custom-flows.ts b/integration/presets/custom-flows.ts index efc01e753a9..236967404b8 100644 --- a/integration/presets/custom-flows.ts +++ b/integration/presets/custom-flows.ts @@ -10,9 +10,9 @@ const reactVite = applicationConfig() .addScript('dev', 'pnpm dev') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm preview') - .addDependency('@clerk/react', linkPackage('react', 'integration')) - .addDependency('@clerk/shared', linkPackage('shared', 'integration')) - .addDependency('@clerk/ui', linkPackage('ui', 'integration')); + .addDependency('@clerk/react', linkPackage('react')) + .addDependency('@clerk/shared', linkPackage('shared')) + .addDependency('@clerk/ui', linkPackage('ui')); export const customFlows = { reactVite, diff --git a/integration/presets/express.ts b/integration/presets/express.ts index f68cf904038..3a0ee82f392 100644 --- a/integration/presets/express.ts +++ b/integration/presets/express.ts @@ -10,9 +10,9 @@ const vite = applicationConfig() .addScript('dev', 'pnpm dev') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm start') - .addDependency('@clerk/express', linkPackage('express', 'integration')) - .addDependency('@clerk/clerk-js', linkPackage('clerk-js', 'integration')) - .addDependency('@clerk/ui', linkPackage('ui', 'integration')); + .addDependency('@clerk/express', linkPackage('express')) + .addDependency('@clerk/clerk-js', linkPackage('clerk-js')) + .addDependency('@clerk/ui', linkPackage('ui')); export const express = { vite, diff --git a/integration/presets/react.ts b/integration/presets/react.ts index 1af28ee9b66..863e17b106c 100644 --- a/integration/presets/react.ts +++ b/integration/presets/react.ts @@ -10,9 +10,9 @@ const cra = applicationConfig() .addScript('dev', 'pnpm start') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm start') - .addDependency('@clerk/react', linkPackage('react', 'integration')) - .addDependency('@clerk/shared', linkPackage('shared', 'integration')) - .addDependency('@clerk/ui', linkPackage('ui', 'integration')); + .addDependency('@clerk/react', linkPackage('react')) + .addDependency('@clerk/shared', linkPackage('shared')) + .addDependency('@clerk/ui', linkPackage('ui')); const vite = cra .clone() diff --git a/integration/tests/custom-flows/sign-in.test.ts b/integration/tests/custom-flows/sign-in.test.ts index c8fe5851790..30a34e0d190 100644 --- a/integration/tests/custom-flows/sign-in.test.ts +++ b/integration/tests/custom-flows/sign-in.test.ts @@ -1,5 +1,3 @@ -import { parsePublishableKey } from '@clerk/shared/keys'; -import { clerkSetup } from '@clerk/testing/playwright'; import { expect, test } from '@playwright/test'; import type { Application } from '../../models/application'; @@ -19,20 +17,6 @@ test.describe('Custom Flows Sign In @custom', () => { await app.withEnv(appConfigs.envs.withEmailCodes); await app.dev(); - const publishableKey = appConfigs.envs.withEmailCodes.publicVariables.get('CLERK_PUBLISHABLE_KEY'); - const secretKey = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_SECRET_KEY'); - const apiUrl = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_API_URL'); - const { frontendApi: frontendApiUrl } = parsePublishableKey(publishableKey); - - await clerkSetup({ - publishableKey, - frontendApiUrl, - secretKey, - // @ts-expect-error - apiUrl, - dotenv: false, - }); - const u = createTestUtils({ app }); fakeUser = u.services.users.createFakeUser({ fictionalEmail: true, diff --git a/integration/tests/custom-flows/sign-up.test.ts b/integration/tests/custom-flows/sign-up.test.ts index 02e16f02051..f7c16143755 100644 --- a/integration/tests/custom-flows/sign-up.test.ts +++ b/integration/tests/custom-flows/sign-up.test.ts @@ -1,5 +1,3 @@ -import { parsePublishableKey } from '@clerk/shared/keys'; -import { clerkSetup } from '@clerk/testing/playwright'; import { expect, test } from '@playwright/test'; import type { Application } from '../../models/application'; @@ -19,20 +17,6 @@ test.describe('Custom Flows Sign Up @custom', () => { await app.withEnv(appConfigs.envs.withEmailCodes); await app.dev(); - const publishableKey = appConfigs.envs.withEmailCodes.publicVariables.get('CLERK_PUBLISHABLE_KEY'); - const secretKey = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_SECRET_KEY'); - const apiUrl = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_API_URL'); - const { frontendApi: frontendApiUrl } = parsePublishableKey(publishableKey); - - await clerkSetup({ - publishableKey, - frontendApiUrl, - secretKey, - // @ts-expect-error - apiUrl, - dotenv: false, - }); - const u = createTestUtils({ app }); fakeUser = u.services.users.createFakeUser({ fictionalEmail: true, diff --git a/integration/tests/global.teardown.ts b/integration/tests/global.teardown.ts index 00e9296dca5..7445ab191c7 100644 --- a/integration/tests/global.teardown.ts +++ b/integration/tests/global.teardown.ts @@ -12,7 +12,7 @@ setup('teardown long running apps', async () => { await killClerkJsHttpServer(); await killClerkUiHttpServer(); - if (appUrl || !constants.CLEANUP) { + if (appUrl || !constants.E2E_CLEANUP) { // if appUrl is provided, it means that the user is running an app manually console.log('Skipping cleanup'); return; diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts index b37527e0f36..d9b308cf355 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/machine-auth/component.test.ts @@ -1,9 +1,10 @@ import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; +import type { Application } from '../../models/application'; import { appConfigs } from '../../presets'; import type { FakeOrganization, FakeUser } from '../../testUtils'; -import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; +import { createTestUtils } from '../../testUtils'; const mockAPIKeysEnvironmentSettings = async ( page: Page, @@ -27,16 +28,21 @@ const mockAPIKeysEnvironmentSettings = async ( }); }; -testAgainstRunningApps({ - withEnv: [appConfigs.envs.withAPIKeys], - withPattern: ['withMachine.next.appRouter'], -})('api keys component @machine', ({ app }) => { +test.describe('api keys component @machine', () => { test.describe.configure({ mode: 'serial' }); + let app: Application; let fakeAdmin: FakeUser; let fakeOrganization: FakeOrganization; test.beforeAll(async () => { + test.setTimeout(90_000); // Wait for app to be ready + app = await appConfigs.next.appRouter.clone().commit(); + + await app.setup(); + await app.withEnv(appConfigs.envs.withAPIKeys); + await app.dev(); + const u = createTestUtils({ app }); fakeAdmin = u.services.users.createFakeUser(); const admin = await u.services.users.createBapiUser(fakeAdmin); From ccbc43c6e36b2abba8b5e876d034d910be384760 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 13:30:11 +0200 Subject: [PATCH 12/15] chore(ui,astro): improve error handling, build config, and code organization UI Package Updates: - Enhanced billing error handling in CheckoutPage by properly flattening ClerkAPIResponseErrors to extract error codes - Updated InvalidPlanScreen to correctly filter and extract errors from ClerkAPIResponseErrors before checking for invalid_plan_change - Removed unused imports from OrganizationProfile components - Simplified SignUpStart appearance usage - Added type-check script to package.json - Updated tsconfig to exclude __tests__ directories - Enhanced rspack dev config with better source maps and disabled tree shaking for easier debugging - Added __BUILD_DISABLE_RHC__ constant to both rspack and tsdown configs Astro Package Updates: - Refactored createClerkInstance to extract clerk-js and clerk-ui loading into separate functions (getClerkJsEntryChunk, getClerkUiEntryChunk) - Improved code clarity with better comments explaining early returns when scripts are already loaded via middleware - Maintained parallel loading of both scripts for optimal performance Root Package Updates: - Added E2E_DEBUG=1 to billing, generic, and machine integration test scripts for better debugging visibility - Updated billing test to use withBillingJwtV2 instead of withBilling - Fixed duplicate E2E_DEBUG flag in express test script --- package.json | 8 +-- .../src/internal/create-clerk-instance.ts | 62 ++++++++++++------- packages/ui/package.json | 3 +- packages/ui/rspack.config.js | 5 +- .../src/components/Checkout/CheckoutPage.tsx | 6 +- packages/ui/src/components/Checkout/parts.tsx | 7 ++- .../InviteMembersScreen.tsx | 3 - .../OrganizationMembers.tsx | 2 +- packages/ui/tsconfig.json | 2 +- packages/ui/tsdown.config.mts | 1 + 10 files changed, 63 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index d5ec6d85fb1..c068833cad3 100644 --- a/package.json +++ b/package.json @@ -34,17 +34,17 @@ "test:integration:ap-flows": "pnpm test:integration:base --grep @ap-flows", "test:integration:astro": "E2E_APP_ID=astro.* pnpm test:integration:base --grep @astro", "test:integration:base": "pnpm playwright test --config integration/playwright.config.ts", - "test:integration:billing": "E2E_APP_ID=withBilling.* pnpm test:integration:base --grep @billing", + "test:integration:billing": "E2E_DEBUG=1 E2E_APP_ID=withBillingJwtV2.* pnpm test:integration:base --grep @billing", "test:integration:cleanup": "pnpm playwright test --config integration/playwright.cleanup.config.ts", "test:integration:custom": "pnpm test:integration:base --grep @custom", "test:integration:deployment:nextjs": "pnpm playwright test --config integration/playwright.deployments.config.ts", "test:integration:expo-web:disabled": "E2E_APP_ID=expo.expo-web pnpm test:integration:base --grep @expo-web", - "test:integration:express": "E2E_DEBUG=1 E2E_APP_ID=express.* pnpm test:integration:base --grep @express", - "test:integration:generic": "E2E_APP_ID=react.vite.*,next.appRouter.withEmailCodes* pnpm test:integration:base --grep @generic", + "test:integration:express": "E2E_DEBUG=1 E2E_DEBUG=1 E2E_APP_ID=express.* pnpm test:integration:base --grep @express", + "test:integration:generic": "E2E_DEBUG=1 E2E_APP_ID=react.vite.*,next.appRouter.withEmailCodes* pnpm test:integration:base --grep @generic", "test:integration:handshake": "DISABLE_WEB_SECURITY=true E2E_APP_1_ENV_KEY=sessions-prod-1 E2E_SESSIONS_APP_1_HOST=multiple-apps-e2e.clerk.app pnpm test:integration:base --grep @handshake", "test:integration:handshake:staging": "DISABLE_WEB_SECURITY=true E2E_APP_1_ENV_KEY=clerkstage-sessions-prod-1 E2E_SESSIONS_APP_1_HOST=clerkstage-sessions-prod-1-e2e.clerk.app pnpm test:integration:base --grep @handshake", "test:integration:localhost": "pnpm test:integration:base --grep @localhost", - "test:integration:machine": "E2E_APP_ID=withMachine.* pnpm test:integration:base --grep @machine", + "test:integration:machine": "E2E_DEBUG=1 pnpm test:integration:base --grep @machine", "test:integration:nextjs": "E2E_APP_ID=next.appRouter.* pnpm test:integration:base --grep @nextjs", "test:integration:nuxt": "E2E_DEBUG=1 E2E_APP_ID=nuxt.node npm run test:integration:base -- --grep @nuxt", "test:integration:quickstart": "E2E_APP_ID=quickstart.* pnpm test:integration:base --grep @quickstart", diff --git a/packages/astro/src/internal/create-clerk-instance.ts b/packages/astro/src/internal/create-clerk-instance.ts index 73a4473c660..e1cbd520144 100644 --- a/packages/astro/src/internal/create-clerk-instance.ts +++ b/packages/astro/src/internal/create-clerk-instance.ts @@ -36,30 +36,20 @@ function createNavigationHandler( const createClerkInstance = runOnce(createClerkInstanceInternal); async function createClerkInstanceInternal(options?: AstroClerkCreateInstanceParams) { - let clerkJSInstance = window.Clerk; - let clerkUiCtor: Promise | undefined; - - if (!clerkJSInstance) { - // Load both clerk-js and clerk-ui in parallel - const clerkPromise = loadClerkJsScript(options); - clerkUiCtor = options?.clerkUiCtor - ? Promise.resolve(options.clerkUiCtor) - : loadClerkUiScript(options).then(() => { - if (!window.__internal_ClerkUiCtor) { - throw new Error('Failed to download latest Clerk UI. Contact support@clerk.com.'); - } - // After the check, TypeScript knows it's defined - return window.__internal_ClerkUiCtor; - }); - - await clerkPromise; - - if (!window.Clerk) { - throw new Error('Failed to download latest ClerkJS. Contact support@clerk.com.'); - } - clerkJSInstance = window.Clerk; + // Load clerk-js and clerk-ui in parallel. + // Both functions return early if the scripts are already loaded + // (e.g., via middleware-injected script tags in the HTML head). + const clerkJsChunk = getClerkJsEntryChunk(options); + const clerkUiCtor = getClerkUiEntryChunk(options); + + await clerkJsChunk; + + if (!window.Clerk) { + throw new Error('Failed to download latest ClerkJS. Contact support@clerk.com.'); } + const clerkJSInstance = window.Clerk; + if (!$clerk.get()) { $clerk.set(clerkJSInstance); } @@ -110,4 +100,32 @@ function updateClerkOptions(options: AstroClerkUpdateOption void (clerk as any).__internal_updateProps(updateOptions); } +/** + * Loads clerk-js script if not already loaded. + * Returns early if window.Clerk already exists. + */ +async function getClerkJsEntryChunk(options?: AstroClerkCreateInstanceParams): Promise { + await loadClerkJsScript(options); +} + +/** + * Gets the ClerkUI constructor, either from options or by loading the script. + * Returns early if window.__internal_ClerkUiCtor already exists. + */ +async function getClerkUiEntryChunk( + options?: AstroClerkCreateInstanceParams, +): Promise { + if (options?.clerkUiCtor) { + return options.clerkUiCtor; + } + + await loadClerkUiScript(options); + + if (!window.__internal_ClerkUiCtor) { + throw new Error('Failed to download latest Clerk UI. Contact support@clerk.com.'); + } + + return window.__internal_ClerkUiCtor; +} + export { createClerkInstance, updateClerkOptions }; diff --git a/packages/ui/package.json b/packages/ui/package.json index 44c4fa809ce..c28f8f99767 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -62,7 +62,8 @@ "showerrors": "tsc", "test:ci": "vitest --maxWorkers=70%", "test:coverage": "vitest --collectCoverage && open coverage/lcov-report/index.html", - "test:disabled": "vitest" + "test:disabled": "vitest", + "type-check": "tsc --noEmit" }, "dependencies": { "@clerk/localizations": "workspace:^", diff --git a/packages/ui/rspack.config.js b/packages/ui/rspack.config.js index fa31862617d..b3129302689 100644 --- a/packages/ui/rspack.config.js +++ b/packages/ui/rspack.config.js @@ -44,6 +44,7 @@ const common = ({ mode, variant }) => { PACKAGE_VERSION: JSON.stringify(packageJSON.version), __PKG_VERSION__: JSON.stringify(packageJSON.version), PACKAGE_NAME: JSON.stringify(packageJSON.name), + __BUILD_DISABLE_RHC__: JSON.stringify(false), }), new rspack.EnvironmentPlugin({ NODE_ENV: mode, @@ -166,7 +167,7 @@ const devConfig = (mode, env) => { rules: [svgLoader(), ...typescriptLoaderDev()], }, plugins: [new ReactRefreshPlugin({ overlay: { sockHost: devUrl.host } })], - devtool: 'eval-cheap-source-map', + devtool: 'eval-source-map', output: { publicPath: `${devUrl.origin}/npm/`, crossOriginLoading: 'anonymous', @@ -175,6 +176,8 @@ const devConfig = (mode, env) => { }, optimization: { minimize: false, + usedExports: false, + providedExports: false, }, devServer: { allowedHosts: ['all'], diff --git a/packages/ui/src/components/Checkout/CheckoutPage.tsx b/packages/ui/src/components/Checkout/CheckoutPage.tsx index 8fd50eabb86..acfe6c84738 100644 --- a/packages/ui/src/components/Checkout/CheckoutPage.tsx +++ b/packages/ui/src/components/Checkout/CheckoutPage.tsx @@ -61,7 +61,11 @@ const FetchStatus = ({ const internalFetchStatus = useMemo(() => { if (errors.global) { - const errorCodes = errors.global.map(e => e.code); + const errorCodes = errors.global.flatMap(e => { + if (e.isClerkApiResponseError()) { + return e.errors.map(e => e.code); + } + }); if (errorCodes.includes('missing_payer_email')) { return 'missing_payer_email'; diff --git a/packages/ui/src/components/Checkout/parts.tsx b/packages/ui/src/components/Checkout/parts.tsx index fa083985990..8a0a882ce59 100644 --- a/packages/ui/src/components/Checkout/parts.tsx +++ b/packages/ui/src/components/Checkout/parts.tsx @@ -41,12 +41,15 @@ export const InvalidPlanScreen = () => { const { planPeriod } = useCheckoutContext(); const { errors } = useCheckout(); - const InvalidPlanError = errors?.global?.find(e => e.code === 'invalid_plan_change'); + const InvalidPlanError = errors?.global + ?.filter(e => e.isClerkApiResponseError()) + .flatMap(e => e.errors) + .find(e => e.code === 'invalid_plan_change'); + if (!InvalidPlanError) { return null; } - // @ts-expect-error - meta is not a property of FieldError const { plan: planFromError, isPlanUpgradePossible } = InvalidPlanError?.meta || {}; return ( diff --git a/packages/ui/src/components/OrganizationProfile/InviteMembersScreen.tsx b/packages/ui/src/components/OrganizationProfile/InviteMembersScreen.tsx index 5d24d2a3641..f6d493b2812 100644 --- a/packages/ui/src/components/OrganizationProfile/InviteMembersScreen.tsx +++ b/packages/ui/src/components/OrganizationProfile/InviteMembersScreen.tsx @@ -1,5 +1,4 @@ import { useOrganization } from '@clerk/shared/react'; -import { runIfFunctionOrReturn } from '@clerk/shared/utils'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; import { FormContainer } from '@/ui/elements/FormContainer'; @@ -7,12 +6,10 @@ import { IconCircle } from '@/ui/elements/IconCircle'; import { SuccessPage } from '@/ui/elements/SuccessPage'; import { useWizard, Wizard } from '../../common'; -import { useOrganizationProfileContext } from '../../contexts'; import { descriptors, Flex, localizationKeys, Text } from '../../customizables'; import { useActionContext } from '../../elements/Action/ActionRoot'; import { Email } from '../../icons'; import { InviteMembersForm } from './InviteMembersForm'; - type InviteMembersScreenProps = { onReset?: () => void; }; diff --git a/packages/ui/src/components/OrganizationProfile/OrganizationMembers.tsx b/packages/ui/src/components/OrganizationProfile/OrganizationMembers.tsx index da882250469..2ce5d4ab645 100644 --- a/packages/ui/src/components/OrganizationProfile/OrganizationMembers.tsx +++ b/packages/ui/src/components/OrganizationProfile/OrganizationMembers.tsx @@ -8,7 +8,7 @@ import { Header } from '@/ui/elements/Header'; import { Tab, TabPanel, TabPanels, Tabs, TabsList } from '@/ui/elements/Tabs'; import { NotificationCountBadge, useProtect } from '../../common'; -import { useEnvironment, useOrganizationProfileContext } from '../../contexts'; +import { useEnvironment } from '../../contexts'; import { Col, descriptors, Flex, localizationKeys } from '../../customizables'; import { Action } from '../../elements/Action'; import { mqu } from '../../styledSystem'; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 8c4013283ec..b1cdc8add12 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -29,6 +29,6 @@ "@/ui*": ["./src/*"] } }, - "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"], + "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "**/__tests__/**"], "include": ["src", "src/global.d.ts"] } diff --git a/packages/ui/tsdown.config.mts b/packages/ui/tsdown.config.mts index d97ec74fb8e..2a7fdac4473 100644 --- a/packages/ui/tsdown.config.mts +++ b/packages/ui/tsdown.config.mts @@ -20,6 +20,7 @@ export default defineConfig(({ watch }) => { PACKAGE_VERSION: `"${uiPackage.version}"`, __PKG_VERSION__: `"${uiPackage.version}"`, __DEV__: `${watch}`, + __BUILD_DISABLE_RHC__: JSON.stringify(false), }, } satisfies Options; From 0688ab46a05222b772e28048c3ac7de7fb64baf0 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 15:55:21 +0200 Subject: [PATCH 13/15] chore(repo): always run e2e tests with debug logging --- .github/workflows/ci.yml | 1 + package.json | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a084be94b70..d0ebaea06f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -481,6 +481,7 @@ jobs: timeout-minutes: 25 run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS env: + E2E_DEBUG: "1" E2E_APP_CLERK_JS_DIR: ${{runner.temp}} E2E_APP_CLERK_UI_DIR: ${{runner.temp}} E2E_CLERK_JS_VERSION: "latest" diff --git a/package.json b/package.json index c068833cad3..24d2f4af5e7 100644 --- a/package.json +++ b/package.json @@ -34,19 +34,19 @@ "test:integration:ap-flows": "pnpm test:integration:base --grep @ap-flows", "test:integration:astro": "E2E_APP_ID=astro.* pnpm test:integration:base --grep @astro", "test:integration:base": "pnpm playwright test --config integration/playwright.config.ts", - "test:integration:billing": "E2E_DEBUG=1 E2E_APP_ID=withBillingJwtV2.* pnpm test:integration:base --grep @billing", + "test:integration:billing": "E2E_APP_ID=withBillingJwtV2.* pnpm test:integration:base --grep @billing", "test:integration:cleanup": "pnpm playwright test --config integration/playwright.cleanup.config.ts", "test:integration:custom": "pnpm test:integration:base --grep @custom", "test:integration:deployment:nextjs": "pnpm playwright test --config integration/playwright.deployments.config.ts", "test:integration:expo-web:disabled": "E2E_APP_ID=expo.expo-web pnpm test:integration:base --grep @expo-web", - "test:integration:express": "E2E_DEBUG=1 E2E_DEBUG=1 E2E_APP_ID=express.* pnpm test:integration:base --grep @express", - "test:integration:generic": "E2E_DEBUG=1 E2E_APP_ID=react.vite.*,next.appRouter.withEmailCodes* pnpm test:integration:base --grep @generic", + "test:integration:express": "E2E_APP_ID=express.* pnpm test:integration:base --grep @express", + "test:integration:generic": "E2E_APP_ID=react.vite.*,next.appRouter.withEmailCodes* pnpm test:integration:base --grep @generic", "test:integration:handshake": "DISABLE_WEB_SECURITY=true E2E_APP_1_ENV_KEY=sessions-prod-1 E2E_SESSIONS_APP_1_HOST=multiple-apps-e2e.clerk.app pnpm test:integration:base --grep @handshake", "test:integration:handshake:staging": "DISABLE_WEB_SECURITY=true E2E_APP_1_ENV_KEY=clerkstage-sessions-prod-1 E2E_SESSIONS_APP_1_HOST=clerkstage-sessions-prod-1-e2e.clerk.app pnpm test:integration:base --grep @handshake", "test:integration:localhost": "pnpm test:integration:base --grep @localhost", - "test:integration:machine": "E2E_DEBUG=1 pnpm test:integration:base --grep @machine", + "test:integration:machine": "pnpm test:integration:base --grep @machine", "test:integration:nextjs": "E2E_APP_ID=next.appRouter.* pnpm test:integration:base --grep @nextjs", - "test:integration:nuxt": "E2E_DEBUG=1 E2E_APP_ID=nuxt.node npm run test:integration:base -- --grep @nuxt", + "test:integration:nuxt": "E2E_APP_ID=nuxt.node npm run test:integration:base -- --grep @nuxt", "test:integration:quickstart": "E2E_APP_ID=quickstart.* pnpm test:integration:base --grep @quickstart", "test:integration:react-router": "E2E_APP_ID=react-router.* pnpm test:integration:base --grep @react-router", "test:integration:sessions": "DISABLE_WEB_SECURITY=true E2E_SESSIONS_APP_1_ENV_KEY=sessions-prod-1 E2E_SESSIONS_APP_2_ENV_KEY=sessions-prod-2 E2E_SESSIONS_APP_1_HOST=multiple-apps-e2e.clerk.app pnpm test:integration:base --grep @sessions", From 882ec2ac7ec8b41d6614cb2b540af296a6f5b296 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Sun, 7 Dec 2025 16:08:13 +0200 Subject: [PATCH 14/15] fix(e2e): add missing clerkJSUrl and clerkUiUrl props to templates Added clerkJSUrl and clerkUiUrl props to integration templates that were missing them. These props are required for CI to use the locally published Clerk packages from Verdaccio instead of the default CDN. Templates updated: - react-router-library - react-router-node - tanstack-react-start --- integration/templates/react-router-library/src/main.tsx | 4 ++++ integration/templates/react-router-node/app/root.tsx | 2 ++ .../templates/tanstack-react-start/src/routes/__root.tsx | 2 ++ 3 files changed, 8 insertions(+) diff --git a/integration/templates/react-router-library/src/main.tsx b/integration/templates/react-router-library/src/main.tsx index 8c1fd62ded1..82c2ee8e68d 100644 --- a/integration/templates/react-router-library/src/main.tsx +++ b/integration/templates/react-router-library/src/main.tsx @@ -6,12 +6,16 @@ import './index.css'; import App from './App.tsx'; const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; +const CLERK_JS_URL = import.meta.env.VITE_CLERK_JS_URL; +const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL; createRoot(document.getElementById('root')!).render( Date: Sun, 7 Dec 2025 20:25:14 +0200 Subject: [PATCH 15/15] ci(e2e): add vercel protection bypass for integration tests Configure Playwright integration tests to bypass Vercel's deployment protection using the x-vercel-protection-bypass header. This enables automated tests to access protected Vercel deployments without manual intervention. Changes include: - Add extraHTTPHeaders to Playwright config with bypass secret - Whitelist VERCEL_AUTOMATION_BYPASS_SECRET in turbo.json for all integration test tasks - Pass bypass secret from GitHub secrets in CI workflow --- .github/workflows/ci.yml | 1 + integration/tests/next-quickstart-keyless.test.ts | 7 +++++++ integration/tests/react-router/library-mode.test.ts | 1 + integration/tests/react-router/pre-middleware.test.ts | 1 + turbo.json | 10 ++++++++-- 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0ebaea06f5..d4c206cd200 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -492,6 +492,7 @@ jobs: CLERK_USE_RQ: ${{ matrix.clerk-use-rq }} INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }} NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/integration/certs/rootCA.pem + VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} - name: Upload test-results if: ${{ cancelled() || failure() }} diff --git a/integration/tests/next-quickstart-keyless.test.ts b/integration/tests/next-quickstart-keyless.test.ts index 571dbf6bd93..d143b60385e 100644 --- a/integration/tests/next-quickstart-keyless.test.ts +++ b/integration/tests/next-quickstart-keyless.test.ts @@ -24,6 +24,13 @@ const mockClaimedInstanceEnvironmentCall = async (page: Page) => { test.describe('Keyless mode @quickstart', () => { test.describe.configure({ mode: 'serial' }); + + test.use({ + extraHTTPHeaders: { + 'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET || '', + }, + }); + let app: Application; let dashboardUrl = 'https://dashboard.clerk.com/'; diff --git a/integration/tests/react-router/library-mode.test.ts b/integration/tests/react-router/library-mode.test.ts index 6f5af6f63b5..ccbf901b9c1 100644 --- a/integration/tests/react-router/library-mode.test.ts +++ b/integration/tests/react-router/library-mode.test.ts @@ -7,6 +7,7 @@ import { createTestUtils } from '../../testUtils'; test.describe('Library Mode basic tests for @react-router', () => { test.describe.configure({ mode: 'parallel' }); + let app: Application; let fakeUser: FakeUser; let fakeOrganization: FakeOrganization; diff --git a/integration/tests/react-router/pre-middleware.test.ts b/integration/tests/react-router/pre-middleware.test.ts index 3cb80691d2d..1fa04bfe5fc 100644 --- a/integration/tests/react-router/pre-middleware.test.ts +++ b/integration/tests/react-router/pre-middleware.test.ts @@ -7,6 +7,7 @@ import { createTestUtils } from '../../testUtils'; test.describe('basic tests for @react-router without middleware', () => { test.describe.configure({ mode: 'parallel' }); + let app: Application; let fakeUser: FakeUser; diff --git a/turbo.json b/turbo.json index 8ef7b479afc..868eb13f839 100644 --- a/turbo.json +++ b/turbo.json @@ -31,7 +31,13 @@ "VERCEL", "VITE_CLERK_*" ], - "globalPassThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN", "ACTIONS_RUNNER_DEBUG", "ACTIONS_STEP_DEBUG"], + "globalPassThroughEnv": [ + "AWS_SECRET_KEY", + "GITHUB_TOKEN", + "ACTIONS_RUNNER_DEBUG", + "ACTIONS_STEP_DEBUG", + "VERCEL_AUTOMATION_BYPASS_SECRET" + ], "tasks": { "build": { "dependsOn": ["^build"], @@ -218,7 +224,7 @@ "outputLogs": "new-only" }, "//#test:integration:quickstart": { - "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], + "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS", "VERCEL_AUTOMATION_BYPASS_SECRET"], "inputs": ["integration/**"], "outputLogs": "new-only" },