Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -101,12 +123,12 @@ export function buildPageLoadSnippet({
const { navigate } = await transitionClient;

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(astro): avoid invalid ui constructor in injected scripts by jescalan · Pull Request #8628 · clerk/javascript · GitHub
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -101,12 +123,12 @@ export function buildPageLoadSnippet({
const { navigate } = await transitionClient;

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(astro): avoid invalid ui constructor in injected scripts by jescalan · Pull Request #8628 · clerk/javascript · GitHub
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -101,12 +123,12 @@ export function buildPageLoadSnippet({
const { navigate } = await transitionClient;

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(astro): avoid invalid ui constructor in injected scripts by jescalan · Pull Request #8628 · clerk/javascript · GitHub
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -101,12 +123,12 @@ export function buildPageLoadSnippet({
const { navigate } = await transitionClient;

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(astro): avoid invalid ui constructor in injected scripts by jescalan · Pull Request #8628 · clerk/javascript · GitHub
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -101,12 +123,12 @@ export function buildPageLoadSnippet({
const { navigate } = await transitionClient;

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(astro): avoid invalid ui constructor in injected scripts by jescalan · Pull Request #8628 · clerk/javascript · GitHub
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -101,12 +123,12 @@ export function buildPageLoadSnippet({
const { navigate } = await transitionClient;

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(astro): avoid invalid ui constructor in injected scripts by jescalan · Pull Request #8628 · clerk/javascript · GitHub
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -101,12 +123,12 @@ export function buildPageLoadSnippet({
const { navigate } = await transitionClient;

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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