diff --git a/.changeset/astro-bundled-ui-serialization.md b/.changeset/astro-bundled-ui-serialization.md new file mode 100644 index 00000000000..23e6ec6f4b4 --- /dev/null +++ b/.changeset/astro-bundled-ui-serialization.md @@ -0,0 +1,5 @@ +--- +'@clerk/astro': patch +--- + +Fix Astro initialization when bundled `ui` or `prefetchUI: false` is passed to the integration. diff --git a/packages/astro/src/integration/__tests__/snippets.test.ts b/packages/astro/src/integration/__tests__/snippets.test.ts new file mode 100644 index 00000000000..e01c75b990c --- /dev/null +++ b/packages/astro/src/integration/__tests__/snippets.test.ts @@ -0,0 +1,67 @@ +import type { ClerkOptions } from '@clerk/shared/types'; +import { describe, expect, it } from 'vitest'; + +import { buildBeforeHydrationSnippet, buildPageLoadSnippet } from '../snippets'; + +const buildSnippetOptions = (internalParams: ClerkOptions) => ({ + command: 'build', + packageName: '@clerk/astro', + buildImportPath: '@clerk/astro/internal', + internalParams, +}); + +describe('integration snippets', () => { + it('imports bundled Clerk UI instead of serializing the constructor for before-hydration scripts', () => { + class ClerkUI {} + + const snippet = buildBeforeHydrationSnippet( + buildSnippetOptions({ + publishableKey: 'pk_test_123', + ui: { + __brand: '__clerkUI', + version: '1.2.3', + ClerkUI, + }, + } as unknown as ClerkOptions), + ); + + expect(snippet).toContain('import { ui as __internal_clerkAstroUi } from "@clerk/ui";'); + expect(snippet).toContain( + 'await runInjectionScript({ ...{"publishableKey":"pk_test_123","ui":{"__brand":"__clerkUI","version":"1.2.3"}}, ui: __internal_clerkAstroUi });', + ); + }); + + it('imports bundled Clerk UI for page-load scripts including view-transition reinitialization', () => { + class ClerkUI {} + + const snippet = buildPageLoadSnippet( + buildSnippetOptions({ + publishableKey: 'pk_test_123', + ui: { + __brand: '__clerkUI', + version: '1.2.3', + ClerkUI, + }, + } as unknown as ClerkOptions), + ); + + expect(snippet).toContain('import { ui as __internal_clerkAstroUi } from "@clerk/ui";'); + expect(snippet).toContain( + '...{ ...{"publishableKey":"pk_test_123","ui":{"__brand":"__clerkUI","version":"1.2.3"}}, ui: __internal_clerkAstroUi },', + ); + expect(snippet).toContain( + 'await runInjectionScript({ ...{"publishableKey":"pk_test_123","ui":{"__brand":"__clerkUI","version":"1.2.3"}}, ui: __internal_clerkAstroUi });', + ); + }); + + it('keeps default snippets free of the bundled UI import', () => { + const snippet = buildBeforeHydrationSnippet( + buildSnippetOptions({ + publishableKey: 'pk_test_123', + } as unknown as ClerkOptions), + ); + + expect(snippet).not.toContain('@clerk/ui'); + expect(snippet).toContain('await runInjectionScript({"publishableKey":"pk_test_123"});'); + }); +}); diff --git a/packages/astro/src/integration/snippets.ts b/packages/astro/src/integration/snippets.ts index 698bf70c87f..e397778147a 100644 --- a/packages/astro/src/integration/snippets.ts +++ b/packages/astro/src/integration/snippets.ts @@ -1,5 +1,21 @@ import type { ClerkOptions } from '@clerk/shared/types'; +function buildInternalParamsExpression(internalParams: ClerkOptions) { + const serializedParams = JSON.stringify(internalParams); + + if (!internalParams.ui) { + return { + imports: '', + params: serializedParams, + }; + } + + return { + imports: 'import { ui as __internal_clerkAstroUi } from "@clerk/ui";', + params: `{ ...${serializedParams}, ui: __internal_clerkAstroUi }`, + }; +} + /** * Creates a snippet that initializes Clerk before client-side framework hydration occurs. * @@ -24,10 +40,13 @@ export function buildBeforeHydrationSnippet({ buildImportPath: string; internalParams: ClerkOptions; }) { + const { imports, params } = buildInternalParamsExpression(internalParams); + return ` ${command === 'dev' ? `console.log("${packageName}","Initialize Clerk: before-hydration")` : ''} + ${imports} import { runInjectionScript } from "${buildImportPath}"; - await runInjectionScript(${JSON.stringify(internalParams)});`; + await runInjectionScript(${params});`; } /** @@ -59,8 +78,11 @@ export function buildPageLoadSnippet({ buildImportPath: string; internalParams: ClerkOptions; }) { + const { imports, params } = buildInternalParamsExpression(internalParams); + return ` ${command === 'dev' ? `console.log("${packageName}","Initialize Clerk: page")` : ''} + ${imports} import { runInjectionScript, swapDocument } from "${buildImportPath}"; // Taken from https://github.com/withastro/astro/blob/e10b03e88c22592fbb42d7245b65c4f486ab736d/packages/astro/src/transitions/router.ts#L39. @@ -89,12 +111,12 @@ export function buildPageLoadSnippet({ const { navigate } = await import('astro:transitions/client'); await runInjectionScript({ - ...${JSON.stringify(internalParams)}, + ...${params}, routerPush: navigate, routerReplace: (url) => navigate(url, { history: 'replace' }), }); }) } else { - await runInjectionScript(${JSON.stringify(internalParams)}); + await runInjectionScript(${params}); }`; } diff --git a/packages/astro/src/internal/__tests__/create-clerk-instance.test.ts b/packages/astro/src/internal/__tests__/create-clerk-instance.test.ts index 8811a9427d0..c265e920058 100644 --- a/packages/astro/src/internal/__tests__/create-clerk-instance.test.ts +++ b/packages/astro/src/internal/__tests__/create-clerk-instance.test.ts @@ -94,4 +94,53 @@ describe('getClerkUIEntryChunk', () => { const loadClerkUIScriptCall = mockLoadClerkUIScript.mock.calls[0]?.[0] as Record; expect(loadClerkUIScriptCall?.__internal_clerkUIUrl).toBeUndefined(); }); + + it('does not pass a ClerkUI promise when prefetchUI is false', async () => { + const mockLoad = vi.fn().mockResolvedValue(undefined); + + mockLoadClerkJSScript.mockImplementation(() => { + (window as any).Clerk = { + load: mockLoad, + addListener: vi.fn(), + }; + return Promise.resolve(null); + }); + + const { createClerkInstance } = await import('../create-clerk-instance'); + + await createClerkInstance({ + publishableKey: 'pk_test_xxx', + prefetchUI: false, + }); + + expect(mockLoadClerkUIScript).not.toHaveBeenCalled(); + const loadCall = mockLoad.mock.calls[0]?.[0] as Record; + expect(loadCall.ui.ClerkUI).toBeUndefined(); + }); + + it('does not pass a ClerkUI promise when ui is a marker object without a constructor', async () => { + const mockLoad = vi.fn().mockResolvedValue(undefined); + + mockLoadClerkJSScript.mockImplementation(() => { + (window as any).Clerk = { + load: mockLoad, + addListener: vi.fn(), + }; + return Promise.resolve(null); + }); + + const { createClerkInstance } = await import('../create-clerk-instance'); + + await createClerkInstance({ + publishableKey: 'pk_test_xxx', + ui: { + __brand: '__clerkUI', + version: '1.2.3', + }, + } as any); + + expect(mockLoadClerkUIScript).not.toHaveBeenCalled(); + const loadCall = mockLoad.mock.calls[0]?.[0] as Record; + expect(loadCall.ui.ClerkUI).toBeUndefined(); + }); }); diff --git a/packages/astro/src/internal/create-clerk-instance.ts b/packages/astro/src/internal/create-clerk-instance.ts index 1a2455fa79d..c3ea0bf20b9 100644 --- a/packages/astro/src/internal/create-clerk-instance.ts +++ b/packages/astro/src/internal/create-clerk-instance.ts @@ -119,9 +119,9 @@ async function getClerkJsEntryChunk(options?: AstroClerkCre * Returns early if window.__internal_ClerkUICtor already exists. * Returns undefined when prefetchUI={false} (no UI needed). */ -async function getClerkUIEntryChunk( +function getClerkUIEntryChunk( options?: AstroClerkCreateInstanceParams, -): Promise { +): ClerkUIConstructor | Promise | undefined { // Support bundled UI via ui.ClerkUI prop if (options?.ui?.ClerkUI) { return options.ui.ClerkUI; @@ -132,6 +132,12 @@ async function getClerkUIEntryChunk( return undefined; } + return loadClerkUIEntryChunk(options); +} + +async function loadClerkUIEntryChunk( + options?: AstroClerkCreateInstanceParams, +): Promise { await loadClerkUIScript(options as any); if (!window.__internal_ClerkUICtor) {