Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/astro-bundled-ui-serialization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/astro': patch
---

Fix Astro initialization when bundled `ui` or `prefetchUI: false` is passed to the integration.
67 changes: 67 additions & 0 deletions packages/astro/src/integration/__tests__/snippets.test.ts
Original file line number Diff line number Diff line change
@@ -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"});');
});
});
28 changes: 25 additions & 3 deletions packages/astro/src/integration/snippets.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand All @@ -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});`;
}

/**
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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});
}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,53 @@ describe('getClerkUIEntryChunk', () => {
const loadClerkUIScriptCall = mockLoadClerkUIScript.mock.calls[0]?.[0] as Record<string, unknown>;
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<string, any>;
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<string, any>;
expect(loadCall.ui.ClerkUI).toBeUndefined();
});
});
10 changes: 8 additions & 2 deletions packages/astro/src/internal/create-clerk-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ async function getClerkJsEntryChunk<TUi extends Ui = Ui>(options?: AstroClerkCre
* Returns early if window.__internal_ClerkUICtor already exists.
* Returns undefined when prefetchUI={false} (no UI needed).
*/
async function getClerkUIEntryChunk<TUi extends Ui = Ui>(
function getClerkUIEntryChunk<TUi extends Ui = Ui>(
options?: AstroClerkCreateInstanceParams<TUi>,
): Promise<ClerkUIConstructor | undefined> {
): ClerkUIConstructor | Promise<ClerkUIConstructor> | undefined {
// Support bundled UI via ui.ClerkUI prop
if (options?.ui?.ClerkUI) {
return options.ui.ClerkUI;
Expand All @@ -132,6 +132,12 @@ async function getClerkUIEntryChunk<TUi extends Ui = Ui>(
return undefined;
}

return loadClerkUIEntryChunk(options);
}

async function loadClerkUIEntryChunk<TUi extends Ui = Ui>(
options?: AstroClerkCreateInstanceParams<TUi>,
): Promise<ClerkUIConstructor> {
await loadClerkUIScript(options as any);

if (!window.__internal_ClerkUICtor) {
Expand Down
Loading