From 14129c8cb9295ab33149449932bdc38fc70ad681 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 11:48:18 -0700 Subject: [PATCH 01/37] chore(shared): Add deriveState function chore(shared): Add deriveState to published files chore(shared): Add deriveState to published files --- packages/shared/package.json | 1 + packages/shared/src/deriveState.ts | 71 ++++++++++++++++++++++++++++++ packages/shared/src/index.ts | 1 + packages/shared/subpaths.mjs | 1 + 4 files changed, 74 insertions(+) create mode 100644 packages/shared/src/deriveState.ts diff --git a/packages/shared/package.json b/packages/shared/package.json index 54d013fab7e..10899d97236 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -51,6 +51,7 @@ "cookie", "date", "deprecated", + "deriveState", "error", "file", "globs", diff --git a/packages/shared/src/deriveState.ts b/packages/shared/src/deriveState.ts new file mode 100644 index 00000000000..6a280fe4a6e --- /dev/null +++ b/packages/shared/src/deriveState.ts @@ -0,0 +1,71 @@ +import type { + ActiveSessionResource, + InitialState, + OrganizationCustomPermissionKey, + OrganizationCustomRoleKey, + OrganizationResource, + Resources, + UserResource, +} from '@clerk/types'; + +export const deriveState = (clerkLoaded: boolean, state: Resources, initialState: InitialState | undefined) => { + if (!clerkLoaded && initialState) { + return deriveFromSsrInitialState(initialState); + } + return deriveFromClientSideState(state); +}; + +const deriveFromSsrInitialState = (initialState: InitialState) => { + const userId = initialState.userId; + const user = initialState.user as UserResource; + const sessionId = initialState.sessionId; + const session = initialState.session as ActiveSessionResource; + const organization = initialState.organization as OrganizationResource; + const orgId = initialState.orgId; + const orgRole = initialState.orgRole as OrganizationCustomRoleKey; + const orgPermissions = initialState.orgPermissions as OrganizationCustomPermissionKey[]; + const orgSlug = initialState.orgSlug; + const actor = initialState.actor; + + return { + userId, + user, + sessionId, + session, + organization, + orgId, + orgRole, + orgPermissions, + orgSlug, + actor, + }; +}; + +const deriveFromClientSideState = (state: Resources) => { + const userId: string | null | undefined = state.user ? state.user.id : state.user; + const user = state.user; + const sessionId: string | null | undefined = state.session ? state.session.id : state.session; + const session = state.session; + const actor = session?.actor; + const organization = state.organization; + const orgId: string | null | undefined = state.organization ? state.organization.id : state.organization; + const orgSlug = organization?.slug; + const membership = organization + ? user?.organizationMemberships?.find(om => om.organization.id === orgId) + : organization; + const orgPermissions = membership ? membership.permissions : membership; + const orgRole = membership ? membership.role : membership; + + return { + userId, + user, + sessionId, + session, + organization, + orgId, + orgRole, + orgSlug, + orgPermissions, + actor, + }; +}; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index f9152c1b5fd..6c1d022fed5 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -17,6 +17,7 @@ export * from './color'; export * from './constants'; export * from './date'; export * from './deprecated'; +export { deriveState } from './deriveState'; export * from './error'; export * from './file'; export { handleValueOrFn } from './handleValueOrFn'; diff --git a/packages/shared/subpaths.mjs b/packages/shared/subpaths.mjs index 37f18538715..110c87ef7fe 100644 --- a/packages/shared/subpaths.mjs +++ b/packages/shared/subpaths.mjs @@ -8,6 +8,7 @@ export const subpathNames = [ 'cookie', 'date', 'deprecated', + 'deriveState', 'error', 'file', 'globs', From 4c14780f941cddb412e1ae704f35a9c04d2a87f4 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 11:55:45 -0700 Subject: [PATCH 02/37] chore(astro): Use shared deriveState function --- packages/astro/src/stores/external.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/stores/external.ts b/packages/astro/src/stores/external.ts index 9ed82c6a8cc..d122767c191 100644 --- a/packages/astro/src/stores/external.ts +++ b/packages/astro/src/stores/external.ts @@ -1,8 +1,8 @@ +import { deriveState } from '@clerk/shared/deriveState'; import { eventMethodCalled } from '@clerk/shared/telemetry'; import { computed, onMount, type Store } from 'nanostores'; import { $clerk, $csrState, $initialState } from './internal'; -import { deriveState } from './utils'; /** * A client side store that is prepopulated with the authentication context during SSR. From b181a0cbdf3b0876bd68dd0f5948bc6cffcbb6f7 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 12:42:42 -0700 Subject: [PATCH 03/37] test(shared): Test deriveState with and without initial state --- .../shared/src/__tests__/deriveState.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 packages/shared/src/__tests__/deriveState.test.ts diff --git a/packages/shared/src/__tests__/deriveState.test.ts b/packages/shared/src/__tests__/deriveState.test.ts new file mode 100644 index 00000000000..e8e9517a31d --- /dev/null +++ b/packages/shared/src/__tests__/deriveState.test.ts @@ -0,0 +1,28 @@ +import type { InitialState, Resources } from '@clerk/types'; + +import { deriveState } from '../deriveState'; + +describe('deriveState', () => { + const mockInitialState = { + userId: 'user_2U330vGHg3llBga8Oi0fzzeNAaG', + sessionId: 'sess_2j1R7g3AUeKMx9M23dBO0XLEQGY', + orgId: 'org_2U330vGHg3llBga8Oi0fzzeNAaG', + } as InitialState; + + const mockResources: Resources = { + client: {} as Resources['client'], + user: { id: mockInitialState.userId } as Resources['user'], + session: { id: mockInitialState.sessionId } as Resources['session'], + organization: { id: mockInitialState.orgId } as Resources['organization'], + }; + + it('uses SSR state when !clerkLoaded and initialState is provided', () => { + expect(deriveState(false, {} as Resources, mockInitialState)).toEqual(mockInitialState); + }); + + it('uses CSR state when clerkLoaded', () => { + const result = deriveState(true, mockResources, undefined); + expect(result.userId).toBe(mockInitialState.userId); + expect(result.orgId).toBe(mockInitialState.orgId); + }); +}); From 0cadd2d6f05fcb066152d9bd7eb8034dbdf04740 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 12:49:42 -0700 Subject: [PATCH 04/37] test(shared): Handle undefined initialState --- packages/shared/src/__tests__/deriveState.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/shared/src/__tests__/deriveState.test.ts b/packages/shared/src/__tests__/deriveState.test.ts index e8e9517a31d..2bfa6ed23b0 100644 --- a/packages/shared/src/__tests__/deriveState.test.ts +++ b/packages/shared/src/__tests__/deriveState.test.ts @@ -23,6 +23,14 @@ describe('deriveState', () => { it('uses CSR state when clerkLoaded', () => { const result = deriveState(true, mockResources, undefined); expect(result.userId).toBe(mockInitialState.userId); + expect(result.sessionId).toBe(mockInitialState.sessionId); expect(result.orgId).toBe(mockInitialState.orgId); }); + + it('handles !clerkLoaded and undefined initialState', () => { + const result = deriveState(false, {} as Resources, undefined); + expect(result.userId).toBeUndefined(); + expect(result.sessionId).toBeUndefined(); + expect(result.orgId).toBeUndefined(); + }); }); From 889fb5ceec47f530ea0fc3f7a56cf07b0ede011e Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 13:43:58 -0700 Subject: [PATCH 05/37] chore(shared): Add and export Clerk script hotload function --- packages/shared/package.json | 1 + packages/shared/src/index.ts | 1 + packages/shared/src/loadClerkJsScript.ts | 100 +++++++++++++++++++++++ packages/shared/subpaths.mjs | 1 + 4 files changed, 103 insertions(+) create mode 100644 packages/shared/src/loadClerkJsScript.ts diff --git a/packages/shared/package.json b/packages/shared/package.json index 10899d97236..1ba6491d1b7 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -59,6 +59,7 @@ "isomorphicAtob", "isomorphicBtoa", "keys", + "loadClerkJsScript", "loadScript", "localStorageBroadcastChannel", "poller", diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 6c1d022fed5..7f36a2313a6 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -24,6 +24,7 @@ export { handleValueOrFn } from './handleValueOrFn'; export { isomorphicAtob } from './isomorphicAtob'; export { isomorphicBtoa } from './isomorphicBtoa'; export * from './keys'; +export * from './loadClerkJsScript'; export { loadScript } from './loadScript'; export { LocalStorageBroadcastChannel } from './localStorageBroadcastChannel'; export * from './poller'; diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts new file mode 100644 index 00000000000..e62d8cec312 --- /dev/null +++ b/packages/shared/src/loadClerkJsScript.ts @@ -0,0 +1,100 @@ +import type { ClerkOptions, SDKMetadata, Without } from '@clerk/types'; + +import { createDevOrStagingUrlCache, parsePublishableKey } from './keys'; +import { loadScript } from './loadScript'; +import { isValidProxyUrl, proxyUrlToAbsoluteURL } from './proxy'; +import { addClerkPrefix } from './url'; + +const FAILED_TO_LOAD_ERROR = 'Clerk: Failed to load Clerk'; +const MISSING_PUBLISHABLE_KEY_ERROR = + 'Clerk: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.'; + +const { isDevOrStagingUrl } = createDevOrStagingUrlCache(); + +type LoadClerkJsScriptOptions = Without & { + publishableKey: string; + clerkJSUrl?: string; + clerkJSVariant?: 'headless' | ''; + clerkJSVersion?: string; + sdkMetadata?: SDKMetadata; + proxyUrl?: string; + domain?: string; +}; + +const loadClerkJsScript = async (opts: LoadClerkJsScriptOptions) => { + const { publishableKey } = opts; + + if (!publishableKey) { + throw new Error(MISSING_PUBLISHABLE_KEY_ERROR); + } + + const existingScript = document.querySelector('script[data-clerk-js-script]'); + + if (existingScript) { + return new Promise((resolve, reject) => { + existingScript.addEventListener('load', () => { + resolve(existingScript); + }); + + existingScript.addEventListener('error', () => { + reject(FAILED_TO_LOAD_ERROR); + }); + }); + } + + return loadScript(clerkJsScriptUrl(opts), { + async: true, + crossOrigin: 'anonymous', + beforeLoad: applyClerkJsScriptAttributes(opts), + }).catch(() => { + throw new Error(FAILED_TO_LOAD_ERROR); + }); +}; + +const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => { + const { clerkJSUrl, clerkJSVariant, clerkJSVersion = '5', proxyUrl, domain, publishableKey } = opts; + + if (clerkJSUrl) { + return clerkJSUrl; + } + + let scriptHost = ''; + if (!!proxyUrl && isValidProxyUrl(proxyUrl)) { + scriptHost = proxyUrlToAbsoluteURL(proxyUrl).replace(/http(s)?:\/\//, ''); + } else if (domain && !isDevOrStagingUrl(parsePublishableKey(publishableKey)?.frontendApi || '')) { + scriptHost = addClerkPrefix(domain); + } else { + scriptHost = parsePublishableKey(publishableKey)?.frontendApi || ''; + } + + const variant = clerkJSVariant ? `${clerkJSVariant.replace(/\.+$/, '')}.` : ''; + return `https://${scriptHost}/npm/@clerk/clerk-js@${clerkJSVersion}/dist/clerk.${variant}browser.js`; +}; + +const buildClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => { + const obj: Record = {}; + + if (options.publishableKey) { + obj['data-clerk-publishable-key'] = options.publishableKey; + } + + if (options.proxyUrl) { + obj['data-clerk-proxy-url'] = options.proxyUrl; + } + + if (options.domain) { + obj['data-clerk-domain'] = options.domain; + } + + return obj; +}; + +const applyClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => (script: HTMLScriptElement) => { + const attributes = buildClerkJsScriptAttributes(options); + for (const attribute in attributes) { + script.setAttribute(attribute, attributes[attribute]); + } +}; + +export { loadClerkJsScript, buildClerkJsScriptAttributes, clerkJsScriptUrl }; +export type { LoadClerkJsScriptOptions }; diff --git a/packages/shared/subpaths.mjs b/packages/shared/subpaths.mjs index 110c87ef7fe..8e327a729e6 100644 --- a/packages/shared/subpaths.mjs +++ b/packages/shared/subpaths.mjs @@ -16,6 +16,7 @@ export const subpathNames = [ 'isomorphicAtob', 'isomorphicBtoa', 'keys', + 'loadClerkJsScript', 'loadScript', 'localStorageBroadcastChannel', 'poller', From 5fc583aa08da0969cb3d58adc7fc4e90b1fe0595 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 19:07:35 -0700 Subject: [PATCH 06/37] test(astro): clean up --- packages/shared/src/__tests__/deriveState.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/__tests__/deriveState.test.ts b/packages/shared/src/__tests__/deriveState.test.ts index 2bfa6ed23b0..244cb3fe857 100644 --- a/packages/shared/src/__tests__/deriveState.test.ts +++ b/packages/shared/src/__tests__/deriveState.test.ts @@ -9,12 +9,12 @@ describe('deriveState', () => { orgId: 'org_2U330vGHg3llBga8Oi0fzzeNAaG', } as InitialState; - const mockResources: Resources = { - client: {} as Resources['client'], - user: { id: mockInitialState.userId } as Resources['user'], - session: { id: mockInitialState.sessionId } as Resources['session'], - organization: { id: mockInitialState.orgId } as Resources['organization'], - }; + const mockResources = { + client: {}, + user: { id: mockInitialState.userId }, + session: { id: mockInitialState.sessionId }, + organization: { id: mockInitialState.orgId }, + } as Resources; it('uses SSR state when !clerkLoaded and initialState is provided', () => { expect(deriveState(false, {} as Resources, mockInitialState)).toEqual(mockInitialState); From 64f19871fcb54c35c28ea59e999c4a34e12e3b72 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 10 Jul 2024 12:07:48 -0700 Subject: [PATCH 07/37] test(astro): add Clerk script loader tests test(astro): Test script loader version test(astro): Add main script loader test --- .../src/__tests__/loadClerkJsScript.test.ts | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 packages/shared/src/__tests__/loadClerkJsScript.test.ts diff --git a/packages/shared/src/__tests__/loadClerkJsScript.test.ts b/packages/shared/src/__tests__/loadClerkJsScript.test.ts new file mode 100644 index 00000000000..af2fb5d58b0 --- /dev/null +++ b/packages/shared/src/__tests__/loadClerkJsScript.test.ts @@ -0,0 +1,126 @@ +import { buildClerkJsScriptAttributes, clerkJsScriptUrl, loadClerkJsScript } from '../loadClerkJsScript'; +import { loadScript } from '../loadScript'; + +jest.mock('../loadScript'); + +describe('loadClerkJsScript()', () => { + const mockPublishableKey = 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk'; + + beforeEach(() => { + jest.clearAllMocks(); + (loadScript as jest.Mock).mockResolvedValue(undefined); + document.querySelector = jest.fn().mockReturnValue(null); + }); + + test('throws error when publishableKey is missing', () => { + expect(() => loadClerkJsScript({} as any)).rejects.toThrow( + 'Clerk: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + ); + }); + + test('loads script when no existing script is found', async () => { + await loadClerkJsScript({ publishableKey: mockPublishableKey }); + + expect(loadScript).toHaveBeenCalledWith( + expect.stringContaining('https://foo-bar-13.clerk.accounts.dev/npm/@clerk/clerk-js@5/dist/clerk.browser.js'), + expect.objectContaining({ + async: true, + crossOrigin: 'anonymous', + beforeLoad: expect.any(Function), + }), + ); + }); + + test('uses existing script when found', async () => { + const mockExistingScript = document.createElement('script'); + document.querySelector = jest.fn().mockReturnValue(mockExistingScript); + + const loadPromise = loadClerkJsScript({ publishableKey: mockPublishableKey }); + mockExistingScript.dispatchEvent(new Event('load')); + + await expect(loadPromise).resolves.toBe(mockExistingScript); + expect(loadScript).not.toHaveBeenCalled(); + }); + + test('rejects when existing script fails to load', async () => { + const mockExistingScript = document.createElement('script'); + document.querySelector = jest.fn().mockReturnValue(mockExistingScript); + + const loadPromise = loadClerkJsScript({ publishableKey: mockPublishableKey }); + mockExistingScript.dispatchEvent(new Event('error')); + + await expect(loadPromise).rejects.toBe('Clerk: Failed to load Clerk'); + expect(loadScript).not.toHaveBeenCalled(); + }); + + test('throws error when loadScript fails', async () => { + (loadScript as jest.Mock).mockRejectedValue(new Error('Script load failed')); + + await expect(loadClerkJsScript({ publishableKey: mockPublishableKey })).rejects.toThrow( + 'Clerk: Failed to load Clerk', + ); + }); +}); + +describe('clerkJsScriptUrl()', () => { + const mockDevPublishableKey = 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk'; + const mockProdPublishableKey = 'pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk'; + + test('returns clerkJSUrl when provided', () => { + const customUrl = 'https://custom.clerk.com/clerk.js'; + const result = clerkJsScriptUrl({ clerkJSUrl: customUrl, publishableKey: mockDevPublishableKey }); + expect(result).toBe(customUrl); + }); + + test('constructs URL correctly for development key', () => { + const result = clerkJsScriptUrl({ publishableKey: mockDevPublishableKey }); + expect(result).toBe('https://foo-bar-13.clerk.accounts.dev/npm/@clerk/clerk-js@5/dist/clerk.browser.js'); + }); + + test('constructs URL correctly for production key', () => { + const result = clerkJsScriptUrl({ publishableKey: mockProdPublishableKey }); + expect(result).toBe('https://example.clerk.accounts.dev/npm/@clerk/clerk-js@5/dist/clerk.browser.js'); + }); + + test('includes clerkJSVariant in URL when provided', () => { + const result = clerkJsScriptUrl({ publishableKey: mockProdPublishableKey, clerkJSVariant: 'headless' }); + expect(result).toBe('https://example.clerk.accounts.dev/npm/@clerk/clerk-js@5/dist/clerk.headless.browser.js'); + }); + + test('uses provided clerkJSVersion', () => { + const result = clerkJsScriptUrl({ publishableKey: mockDevPublishableKey, clerkJSVersion: '6' }); + expect(result).toContain('/npm/@clerk/clerk-js@6/'); + }); +}); + +describe('buildClerkJsScriptAttributes()', () => { + const mockPublishableKey = 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk'; + const mockProxyUrl = 'https://proxy.clerk.com'; + const mockDomain = 'custom.com'; + + test.each([ + [ + 'all options', + { publishableKey: mockPublishableKey, proxyUrl: mockProxyUrl, domain: mockDomain }, + { + 'data-clerk-publishable-key': mockPublishableKey, + 'data-clerk-proxy-url': mockProxyUrl, + 'data-clerk-domain': mockDomain, + }, + ], + [ + 'only publishableKey', + { publishableKey: mockPublishableKey }, + { 'data-clerk-publishable-key': mockPublishableKey }, + ], + [ + 'publishableKey and proxyUrl', + { publishableKey: mockPublishableKey, proxyUrl: mockProxyUrl }, + { 'data-clerk-publishable-key': mockPublishableKey, 'data-clerk-proxy-url': mockProxyUrl }, + ], + ['no options', {}, {}], + ])('returns correct attributes with %s', (_, input, expected) => { + // @ts-ignore input loses correct type because of empty object + expect(buildClerkJsScriptAttributes(input)).toEqual(expected); + }); +}); From 133615e4319b864f9c5f65c633fd79b4b9517332 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 10 Jul 2024 14:13:16 -0700 Subject: [PATCH 08/37] chore(astro): Use shared script url builder when hotloading script --- .../src/server/build-clerk-hotload-script.ts | 45 +++---------------- 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/packages/astro/src/server/build-clerk-hotload-script.ts b/packages/astro/src/server/build-clerk-hotload-script.ts index 4b0ed219383..87adac68779 100644 --- a/packages/astro/src/server/build-clerk-hotload-script.ts +++ b/packages/astro/src/server/build-clerk-hotload-script.ts @@ -1,43 +1,8 @@ -import { createDevOrStagingUrlCache, parsePublishableKey } from '@clerk/shared/keys'; -import { isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/shared/proxy'; -import { addClerkPrefix } from '@clerk/shared/url'; +import { clerkJsScriptUrl } from '@clerk/shared/loadClerkJsScript'; import type { APIContext } from 'astro'; -import { versionSelector } from '../internal/utils/versionSelector'; import { getSafeEnv } from './get-safe-env'; -const { isDevOrStagingUrl } = createDevOrStagingUrlCache(); - -type BuildClerkJsScriptOptions = { - proxyUrl: string; - domain: string; - clerkJSUrl?: string; - clerkJSVariant?: 'headless' | ''; - clerkJSVersion?: string; - publishableKey: string; -}; - -const clerkJsScriptUrl = (opts: BuildClerkJsScriptOptions) => { - const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts; - - if (clerkJSUrl) { - return clerkJSUrl; - } - - let scriptHost = ''; - if (!!proxyUrl && isValidProxyUrl(proxyUrl)) { - scriptHost = proxyUrlToAbsoluteURL(proxyUrl).replace(/http(s)?:\/\//, ''); - } else if (domain && !isDevOrStagingUrl(parsePublishableKey(publishableKey)?.frontendApi || '')) { - scriptHost = addClerkPrefix(domain); - } else { - scriptHost = parsePublishableKey(publishableKey)?.frontendApi || ''; - } - - const variant = clerkJSVariant ? `${clerkJSVariant.replace(/\.+$/, '')}.` : ''; - const version = versionSelector(clerkJSVersion); - return `https://${scriptHost}/npm/@clerk/clerk-js@${version}/dist/clerk.${variant}browser.js`; -}; - function buildClerkHotloadScript(locals: APIContext['locals']) { const publishableKey = getSafeEnv(locals).pk!; const proxyUrl = getSafeEnv(locals).proxyUrl!; @@ -51,10 +16,10 @@ function buildClerkHotloadScript(locals: APIContext['locals']) { publishableKey, }); return ` - \n`; } From 3fca39162ab69978ee7535c5ca2f7167b6c13e8c Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 10 Jul 2024 15:40:30 -0700 Subject: [PATCH 10/37] chore(astro): Use shared clerk-js hotload script --- .../src/internal/utils/loadClerkJSScript.ts | 17 --------- .../src/internal/utils/versionSelector.ts | 9 ----- .../src/server/build-clerk-hotload-script.ts | 38 ------------------- packages/astro/src/server/clerk-middleware.ts | 2 - 4 files changed, 66 deletions(-) delete mode 100644 packages/astro/src/internal/utils/loadClerkJSScript.ts delete mode 100644 packages/astro/src/internal/utils/versionSelector.ts delete mode 100644 packages/astro/src/server/build-clerk-hotload-script.ts diff --git a/packages/astro/src/internal/utils/loadClerkJSScript.ts b/packages/astro/src/internal/utils/loadClerkJSScript.ts deleted file mode 100644 index 6d250441339..00000000000 --- a/packages/astro/src/internal/utils/loadClerkJSScript.ts +++ /dev/null @@ -1,17 +0,0 @@ -const FAILED_TO_FIND_CLERK_SCRIPT = 'Clerk: Failed find clerk-js script'; - -// TODO-SHARED: Something similar exists inside clerk-react -export const waitForClerkScript = () => { - return new Promise((resolve, reject) => { - const script = document.querySelector('script[data-clerk-script]'); - - if (!script) { - return reject(FAILED_TO_FIND_CLERK_SCRIPT); - } - - script.addEventListener('load', () => { - script.remove(); - resolve(script); - }); - }); -}; diff --git a/packages/astro/src/internal/utils/versionSelector.ts b/packages/astro/src/internal/utils/versionSelector.ts deleted file mode 100644 index 12b8d4c2fdd..00000000000 --- a/packages/astro/src/internal/utils/versionSelector.ts +++ /dev/null @@ -1,9 +0,0 @@ -const HARDCODED_LATEST_CLERK_JS_VERSION = '5'; - -export const versionSelector = (clerkJSVersion: string | undefined): string => { - if (clerkJSVersion) { - return clerkJSVersion; - } - - return HARDCODED_LATEST_CLERK_JS_VERSION; -}; diff --git a/packages/astro/src/server/build-clerk-hotload-script.ts b/packages/astro/src/server/build-clerk-hotload-script.ts deleted file mode 100644 index 0e2f1e0da14..00000000000 --- a/packages/astro/src/server/build-clerk-hotload-script.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { buildClerkJsScriptAttributes, clerkJsScriptUrl } from '@clerk/shared/loadClerkJsScript'; -import type { APIContext } from 'astro'; - -import { getSafeEnv } from './get-safe-env'; - -function buildClerkHotloadScript(locals: APIContext['locals']) { - const publishableKey = getSafeEnv(locals).pk!; - const proxyUrl = getSafeEnv(locals).proxyUrl!; - const domain = getSafeEnv(locals).domain!; - - const scriptSrc = clerkJsScriptUrl({ - clerkJSUrl: getSafeEnv(locals).clerkJsUrl, - clerkJSVariant: getSafeEnv(locals).clerkJsVariant, - clerkJSVersion: getSafeEnv(locals).clerkJsVersion, - domain, - proxyUrl, - publishableKey, - }); - - const attributes = buildClerkJsScriptAttributes({ - publishableKey, - proxyUrl, - domain, - }); - const attributesString = Object.entries(attributes) - .map(([key, value]) => `${key}="${value}"`) - .join(' '); - - return ` - \n`; -} - -export { buildClerkHotloadScript }; diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index 0dc2b8ea5f5..30ab5142505 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -8,7 +8,6 @@ import type { APIContext } from 'astro'; // @ts-ignore import { authAsyncStorage } from '#async-local-storage'; -import { buildClerkHotloadScript } from './build-clerk-hotload-script'; import { clerkClient } from './clerk-client'; import { createCurrentUser } from './current-user'; import { getAuth } from './get-auth'; @@ -287,7 +286,6 @@ async function decorateRequest( const clerkSafeEnvVariables = encoder.encode( `\n`, ); - const hotloadScript = encoder.encode(buildClerkHotloadScript(locals)); const stream = res.body!.pipeThrough( new TransformStream({ From 142246ce243719ff0e0a4db95bcc81dda24915af Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 10 Jul 2024 16:09:17 -0700 Subject: [PATCH 11/37] chore(astro): Clean up type ignores --- packages/astro/src/client/bundled.ts | 52 +++++++++++++++++++ .../internal/merge-env-vars-with-params.ts | 2 + 2 files changed, 54 insertions(+) create mode 100644 packages/astro/src/client/bundled.ts diff --git a/packages/astro/src/client/bundled.ts b/packages/astro/src/client/bundled.ts new file mode 100644 index 00000000000..151505bac0f --- /dev/null +++ b/packages/astro/src/client/bundled.ts @@ -0,0 +1,52 @@ +import { Clerk } from '@clerk/clerk-js'; + +import { $clerk, $csrState } from '../stores/internal'; +import type { AstroClerkCreateInstanceParams, AstroClerkUpdateOptions } from '../types'; +import { mountAllClerkAstroJSComponents } from './mount-clerk-astro-js-components'; +import { runOnce } from './run-once'; +import type { CreateClerkInstanceInternalFn } from './types'; + +let initOptions: AstroClerkCreateInstanceParams | undefined; + +/** + * Prevents firing clerk.load multiple times + */ +export const createClerkInstance: CreateClerkInstanceInternalFn = runOnce(createClerkInstanceInternal); + +export function createClerkInstanceInternal(options?: AstroClerkCreateInstanceParams) { + let clerkJSInstance = window.Clerk as unknown as Clerk; + if (!clerkJSInstance) { + clerkJSInstance = new Clerk(options!.publishableKey); + $clerk.set(clerkJSInstance); + window.Clerk = clerkJSInstance; + } + + initOptions = options; + return clerkJSInstance + .load(options) + .then(() => { + $csrState.setKey('isLoaded', true); + + mountAllClerkAstroJSComponents(); + + clerkJSInstance.addListener(payload => { + $csrState.setKey('client', payload.client); + $csrState.setKey('user', payload.user); + $csrState.setKey('session', payload.session); + $csrState.setKey('organization', payload.organization); + }); + }) + .catch(() => {}); +} + +export function updateClerkOptions(options: AstroClerkUpdateOptions) { + const clerk = $clerk.get(); + if (!clerk) { + throw new Error('Missing clerk instance'); + } + //@ts-ignore + clerk.__unstable__updateProps({ + options: { ...initOptions, ...options }, + appearance: { ...initOptions?.appearance, ...options.appearance }, + }); +} diff --git a/packages/astro/src/internal/merge-env-vars-with-params.ts b/packages/astro/src/internal/merge-env-vars-with-params.ts index c5498c1e0da..2160191b82b 100644 --- a/packages/astro/src/internal/merge-env-vars-with-params.ts +++ b/packages/astro/src/internal/merge-env-vars-with-params.ts @@ -17,6 +17,8 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish ...rest } = params || {}; + // We have an eslint config that requires us to declare env variables in the turbo.json file. + // We're skipping/disabling it below as we use it only for the Astro integration. return { signInUrl: paramSignIn || import.meta.env.PUBLIC_CLERK_SIGN_IN_URL, signUpUrl: paramSignUp || import.meta.env.PUBLIC_CLERK_SIGN_UP_URL, From 09f2fde86278f3d0f59c913881c9c48b5c53f14a Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Wed, 10 Jul 2024 18:55:29 -0700 Subject: [PATCH 12/37] chore(astro,shared): Add changeset --- .changeset/nasty-baboons-cheer.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/nasty-baboons-cheer.md diff --git a/.changeset/nasty-baboons-cheer.md b/.changeset/nasty-baboons-cheer.md new file mode 100644 index 00000000000..9d2f49a215c --- /dev/null +++ b/.changeset/nasty-baboons-cheer.md @@ -0,0 +1,6 @@ +--- +"@clerk/astro": patch +"@clerk/shared": patch +--- + +Introduce functions that can be reused across front-end SDKs From ea5f60744124f42d0f6fa277851c08eb5cbe6658 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 11 Jul 2024 09:48:21 -0700 Subject: [PATCH 13/37] chore(shared): Add and export version selector from react package --- packages/shared/global.d.ts | 1 + packages/shared/jest.config.js | 5 ++ packages/shared/package.json | 1 + .../src/__tests__/versionSelector.test.ts | 50 +++++++++++++++++++ packages/shared/src/index.ts | 1 + packages/shared/src/loadClerkJsScript.ts | 4 +- packages/shared/src/versionSelector.ts | 34 +++++++++++++ packages/shared/subpaths.mjs | 1 + packages/shared/tsup.config.ts | 2 + 9 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 packages/shared/src/__tests__/versionSelector.test.ts create mode 100644 packages/shared/src/versionSelector.ts diff --git a/packages/shared/global.d.ts b/packages/shared/global.d.ts index 8c7a4b73b56..d080e40fece 100644 --- a/packages/shared/global.d.ts +++ b/packages/shared/global.d.ts @@ -2,5 +2,6 @@ export {}; declare global { const PACKAGE_VERSION: string; + const JS_PACKAGE_VERSION: string; const __DEV__: boolean; } diff --git a/packages/shared/jest.config.js b/packages/shared/jest.config.js index f9da269204a..fe05710e000 100644 --- a/packages/shared/jest.config.js +++ b/packages/shared/jest.config.js @@ -1,4 +1,5 @@ const { name } = require('./package.json'); +const { version: clerkJsVersion } = require('../clerk-js/package.json'); /** @type {import('ts-jest').JestConfigWithTsJest} */ const config = { @@ -17,6 +18,10 @@ const config = { transform: { '^.+\\.m?tsx?$': ['ts-jest', { tsconfig: 'tsconfig.test.json', diagnostics: false }], }, + + globals: { + JS_PACKAGE_VERSION: clerkJsVersion, + }, }; module.exports = config; diff --git a/packages/shared/package.json b/packages/shared/package.json index 1ba6491d1b7..d7346b81324 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -66,6 +66,7 @@ "proxy", "underscore", "url", + "versionSelector", "react", "constants", "apiUrlFromPublishableKey", diff --git a/packages/shared/src/__tests__/versionSelector.test.ts b/packages/shared/src/__tests__/versionSelector.test.ts new file mode 100644 index 00000000000..40e2812e25d --- /dev/null +++ b/packages/shared/src/__tests__/versionSelector.test.ts @@ -0,0 +1,50 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { versionSelector } from '../versionSelector'; + +describe('versionSelector', () => { + it('should return the clerkJSVersion if it is provided', () => { + expect(versionSelector('1.0.0')).toEqual('1.0.0'); + }); + + it('should use the major clerkJS version if there is no prerelease tag', () => { + // @ts-ignore + global.JS_PACKAGE_VERSION = '2.0.0'; + + expect(versionSelector(undefined)).toEqual('2'); + }); + + it('should use the prerelease tag when it is not snapshot', () => { + // @ts-ignore + global.JS_PACKAGE_VERSION = '2.0.0-next.0'; + + expect(versionSelector(undefined)).toEqual('next'); + }); + + it('should use the exact JS version if tag is snapshot', () => { + // @ts-ignore + global.JS_PACKAGE_VERSION = '2.0.0-snapshot.0'; + + expect(versionSelector(undefined)).toEqual('2.0.0-snapshot.0'); + }); + + // We replaced semver with 2 custom regexes + // so we're testing the same cases as semver tests + // https://github.com/npm/node-semver/blob/main/test/functions/prerelease.js + // https://github.com/npm/node-semver/blob/main/test/functions/major.js + test.each([ + ['1.2.3', 1], + [' 1.2.3 ', 1], + [' 2.2.3-4 ', 4], + [' 3.2.3-pre ', 'pre'], + ['v5.2.3', 5], + [' v8.2.3 ', 8], + ['\t13.2.3', 13], + ['1.2.2-alpha.1', 'alpha'], + ['1.2.2-beta.1', 'beta'], + ['1.2.2-rc.1', 'rc'], + ['1.2.2', 1], + ['1.2.2-pre', 'pre'], + ])('versionSelector(%s) should return %i', (version, expected) => { + expect(versionSelector(undefined, version)).toEqual(expected.toString()); + }); +}); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 7f36a2313a6..d1c7723d9b7 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -31,6 +31,7 @@ export * from './poller'; export * from './proxy'; export * from './underscore'; export * from './url'; +export { versionSelector } from './versionSelector'; export * from './object'; export * from './logger'; export { createWorkerTimers } from './workerTimers'; diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index e62d8cec312..f09bf99b760 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -4,6 +4,7 @@ import { createDevOrStagingUrlCache, parsePublishableKey } from './keys'; import { loadScript } from './loadScript'; import { isValidProxyUrl, proxyUrlToAbsoluteURL } from './proxy'; import { addClerkPrefix } from './url'; +import { versionSelector } from './versionSelector'; const FAILED_TO_LOAD_ERROR = 'Clerk: Failed to load Clerk'; const MISSING_PUBLISHABLE_KEY_ERROR = @@ -68,7 +69,8 @@ const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => { } const variant = clerkJSVariant ? `${clerkJSVariant.replace(/\.+$/, '')}.` : ''; - return `https://${scriptHost}/npm/@clerk/clerk-js@${clerkJSVersion}/dist/clerk.${variant}browser.js`; + const version = versionSelector(clerkJSVersion); + return `https://${scriptHost}/npm/@clerk/clerk-js@${version}/dist/clerk.${variant}browser.js`; }; const buildClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => { diff --git a/packages/shared/src/versionSelector.ts b/packages/shared/src/versionSelector.ts new file mode 100644 index 00000000000..6bc045b6e37 --- /dev/null +++ b/packages/shared/src/versionSelector.ts @@ -0,0 +1,34 @@ +/** + * This version selector is a bit complicated, so here is the flow: + * 1. Use the clerkJSVersion prop on the provider + * 2. Use the exact `@clerk/clerk-js` version if tag is snapshot + * 3. Use the prerelease tag of `@clerk/clerk-js` + * 4. Fallback to the major version of `@clerk/clerk-js` + * @param clerkJSVersion - The optional clerkJSVersion prop on the provider + * @param packageVersion - The version of `@clerk/clerk-js` that will be used if an explicit version is not provided + * @returns The npm tag, version or major version to use + */ +export const versionSelector = (clerkJSVersion: string | undefined, packageVersion = JS_PACKAGE_VERSION) => { + if (clerkJSVersion) { + return clerkJSVersion; + } + + const prereleaseTag = getPrereleaseTag(packageVersion); + if (prereleaseTag) { + if (prereleaseTag === 'snapshot') { + return JS_PACKAGE_VERSION; + } + + return prereleaseTag; + } + + return getMajorVersion(packageVersion); +}; + +const getPrereleaseTag = (packageVersion: string) => + packageVersion + .trim() + .replace(/^v/, '') + .match(/-(.+?)(\.|$)/)?.[1]; + +const getMajorVersion = (packageVersion: string) => packageVersion.trim().replace(/^v/, '').split('.')[0]; diff --git a/packages/shared/subpaths.mjs b/packages/shared/subpaths.mjs index 8e327a729e6..3ec9dc42cdb 100644 --- a/packages/shared/subpaths.mjs +++ b/packages/shared/subpaths.mjs @@ -23,6 +23,7 @@ export const subpathNames = [ 'proxy', 'underscore', 'url', + 'versionSelector', 'constants', 'apiUrlFromPublishableKey', 'telemetry', diff --git a/packages/shared/tsup.config.ts b/packages/shared/tsup.config.ts index 8c912b55a08..12bb36aac30 100644 --- a/packages/shared/tsup.config.ts +++ b/packages/shared/tsup.config.ts @@ -3,6 +3,7 @@ import { transform } from 'esbuild'; import { readFile } from 'fs/promises'; import { defineConfig } from 'tsup'; +import { version as clerkJsVersion } from '../clerk-js/package.json'; import { name, version } from './package.json'; export default defineConfig(overrideOptions => { @@ -21,6 +22,7 @@ export default defineConfig(overrideOptions => { define: { PACKAGE_NAME: `"${name}"`, PACKAGE_VERSION: `"${version}"`, + JS_PACKAGE_VERSION: `"${clerkJsVersion}"`, __DEV__: `${isWatch}`, }, }; From cb283c665fb3817c6bb474906cc134c62de1e4af Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 30 Jul 2024 10:30:31 -0700 Subject: [PATCH 14/37] chore(astro): Fix merge conflicts chore: Remove bundled file chore(astro): Add back missing declarations --- packages/astro/src/client/bundled.ts | 52 --------------- .../internal/merge-env-vars-with-params.ts | 2 - .../src/internal/utils/loadClerkJsScript.ts | 17 +++++ .../src/internal/utils/versionSelector.ts | 9 +++ .../src/server/build-clerk-hotload-script.ts | 64 +++++++++++++++++++ packages/astro/src/server/clerk-middleware.ts | 2 + 6 files changed, 92 insertions(+), 54 deletions(-) delete mode 100644 packages/astro/src/client/bundled.ts create mode 100644 packages/astro/src/internal/utils/loadClerkJsScript.ts create mode 100644 packages/astro/src/internal/utils/versionSelector.ts create mode 100644 packages/astro/src/server/build-clerk-hotload-script.ts diff --git a/packages/astro/src/client/bundled.ts b/packages/astro/src/client/bundled.ts deleted file mode 100644 index 151505bac0f..00000000000 --- a/packages/astro/src/client/bundled.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Clerk } from '@clerk/clerk-js'; - -import { $clerk, $csrState } from '../stores/internal'; -import type { AstroClerkCreateInstanceParams, AstroClerkUpdateOptions } from '../types'; -import { mountAllClerkAstroJSComponents } from './mount-clerk-astro-js-components'; -import { runOnce } from './run-once'; -import type { CreateClerkInstanceInternalFn } from './types'; - -let initOptions: AstroClerkCreateInstanceParams | undefined; - -/** - * Prevents firing clerk.load multiple times - */ -export const createClerkInstance: CreateClerkInstanceInternalFn = runOnce(createClerkInstanceInternal); - -export function createClerkInstanceInternal(options?: AstroClerkCreateInstanceParams) { - let clerkJSInstance = window.Clerk as unknown as Clerk; - if (!clerkJSInstance) { - clerkJSInstance = new Clerk(options!.publishableKey); - $clerk.set(clerkJSInstance); - window.Clerk = clerkJSInstance; - } - - initOptions = options; - return clerkJSInstance - .load(options) - .then(() => { - $csrState.setKey('isLoaded', true); - - mountAllClerkAstroJSComponents(); - - clerkJSInstance.addListener(payload => { - $csrState.setKey('client', payload.client); - $csrState.setKey('user', payload.user); - $csrState.setKey('session', payload.session); - $csrState.setKey('organization', payload.organization); - }); - }) - .catch(() => {}); -} - -export function updateClerkOptions(options: AstroClerkUpdateOptions) { - const clerk = $clerk.get(); - if (!clerk) { - throw new Error('Missing clerk instance'); - } - //@ts-ignore - clerk.__unstable__updateProps({ - options: { ...initOptions, ...options }, - appearance: { ...initOptions?.appearance, ...options.appearance }, - }); -} diff --git a/packages/astro/src/internal/merge-env-vars-with-params.ts b/packages/astro/src/internal/merge-env-vars-with-params.ts index 2160191b82b..c5498c1e0da 100644 --- a/packages/astro/src/internal/merge-env-vars-with-params.ts +++ b/packages/astro/src/internal/merge-env-vars-with-params.ts @@ -17,8 +17,6 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish ...rest } = params || {}; - // We have an eslint config that requires us to declare env variables in the turbo.json file. - // We're skipping/disabling it below as we use it only for the Astro integration. return { signInUrl: paramSignIn || import.meta.env.PUBLIC_CLERK_SIGN_IN_URL, signUpUrl: paramSignUp || import.meta.env.PUBLIC_CLERK_SIGN_UP_URL, diff --git a/packages/astro/src/internal/utils/loadClerkJsScript.ts b/packages/astro/src/internal/utils/loadClerkJsScript.ts new file mode 100644 index 00000000000..6d250441339 --- /dev/null +++ b/packages/astro/src/internal/utils/loadClerkJsScript.ts @@ -0,0 +1,17 @@ +const FAILED_TO_FIND_CLERK_SCRIPT = 'Clerk: Failed find clerk-js script'; + +// TODO-SHARED: Something similar exists inside clerk-react +export const waitForClerkScript = () => { + return new Promise((resolve, reject) => { + const script = document.querySelector('script[data-clerk-script]'); + + if (!script) { + return reject(FAILED_TO_FIND_CLERK_SCRIPT); + } + + script.addEventListener('load', () => { + script.remove(); + resolve(script); + }); + }); +}; diff --git a/packages/astro/src/internal/utils/versionSelector.ts b/packages/astro/src/internal/utils/versionSelector.ts new file mode 100644 index 00000000000..12b8d4c2fdd --- /dev/null +++ b/packages/astro/src/internal/utils/versionSelector.ts @@ -0,0 +1,9 @@ +const HARDCODED_LATEST_CLERK_JS_VERSION = '5'; + +export const versionSelector = (clerkJSVersion: string | undefined): string => { + if (clerkJSVersion) { + return clerkJSVersion; + } + + return HARDCODED_LATEST_CLERK_JS_VERSION; +}; diff --git a/packages/astro/src/server/build-clerk-hotload-script.ts b/packages/astro/src/server/build-clerk-hotload-script.ts new file mode 100644 index 00000000000..0bc7f5b8ea7 --- /dev/null +++ b/packages/astro/src/server/build-clerk-hotload-script.ts @@ -0,0 +1,64 @@ +import { createDevOrStagingUrlCache, parsePublishableKey } from '@clerk/shared/keys'; +import { isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/shared/proxy'; +import { addClerkPrefix } from '@clerk/shared/url'; +import type { APIContext } from 'astro'; + +import { versionSelector } from '../internal/utils/versionSelector'; +import { getSafeEnv } from './get-safe-env'; + +const { isDevOrStagingUrl } = createDevOrStagingUrlCache(); + +type BuildClerkJsScriptOptions = { + proxyUrl: string; + domain: string; + clerkJSUrl?: string; + clerkJSVariant?: 'headless' | ''; + clerkJSVersion?: string; + publishableKey: string; +}; + +const clerkJsScriptUrl = (opts: BuildClerkJsScriptOptions) => { + const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts; + + if (clerkJSUrl) { + return clerkJSUrl; + } + + let scriptHost = ''; + if (!!proxyUrl && isValidProxyUrl(proxyUrl)) { + scriptHost = proxyUrlToAbsoluteURL(proxyUrl).replace(/http(s)?:\/\//, ''); + } else if (domain && !isDevOrStagingUrl(parsePublishableKey(publishableKey)?.frontendApi || '')) { + scriptHost = addClerkPrefix(domain); + } else { + scriptHost = parsePublishableKey(publishableKey)?.frontendApi || ''; + } + + const variant = clerkJSVariant ? `${clerkJSVariant.replace(/\.+$/, '')}.` : ''; + const version = versionSelector(clerkJSVersion); + return `https://${scriptHost}/npm/@clerk/clerk-js@${version}/dist/clerk.${variant}browser.js`; +}; + +function buildClerkHotloadScript(locals: APIContext['locals']) { + const publishableKey = getSafeEnv(locals).pk!; + const proxyUrl = getSafeEnv(locals).proxyUrl!; + const domain = getSafeEnv(locals).domain!; + const scriptSrc = clerkJsScriptUrl({ + clerkJSUrl: getSafeEnv(locals).clerkJsUrl, + clerkJSVariant: getSafeEnv(locals).clerkJsVariant, + clerkJSVersion: getSafeEnv(locals).clerkJsVersion, + domain, + proxyUrl, + publishableKey, + }); + return ` + \n`; +} + +export { buildClerkHotloadScript }; diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index 30ab5142505..0dc2b8ea5f5 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -8,6 +8,7 @@ import type { APIContext } from 'astro'; // @ts-ignore import { authAsyncStorage } from '#async-local-storage'; +import { buildClerkHotloadScript } from './build-clerk-hotload-script'; import { clerkClient } from './clerk-client'; import { createCurrentUser } from './current-user'; import { getAuth } from './get-auth'; @@ -286,6 +287,7 @@ async function decorateRequest( const clerkSafeEnvVariables = encoder.encode( `\n`, ); + const hotloadScript = encoder.encode(buildClerkHotloadScript(locals)); const stream = res.body!.pipeThrough( new TransformStream({ From d5ed8226c6c231dba032abc9c2dd57a39cead849 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 30 Jul 2024 10:50:01 -0700 Subject: [PATCH 15/37] chore(astro): Fix incorrect file name --- .../internal/utils/{loadClerkJsScript.ts => loadClerkJSScript.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/astro/src/internal/utils/{loadClerkJsScript.ts => loadClerkJSScript.ts} (100%) diff --git a/packages/astro/src/internal/utils/loadClerkJsScript.ts b/packages/astro/src/internal/utils/loadClerkJSScript.ts similarity index 100% rename from packages/astro/src/internal/utils/loadClerkJsScript.ts rename to packages/astro/src/internal/utils/loadClerkJSScript.ts From 41038f8bf5e9c6384e404d393f13d00801fa8840 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 30 Jul 2024 14:41:36 -0700 Subject: [PATCH 16/37] chore(shared): Remove hardcoded version from clerk js script loader --- packages/shared/src/loadClerkJsScript.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index f09bf99b760..d350ab53069 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -53,7 +53,7 @@ const loadClerkJsScript = async (opts: LoadClerkJsScriptOptions) => { }; const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => { - const { clerkJSUrl, clerkJSVariant, clerkJSVersion = '5', proxyUrl, domain, publishableKey } = opts; + const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts; if (clerkJSUrl) { return clerkJSUrl; From 1eaee7934030d2ceb85e778468724ac926579b7d Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 30 Jul 2024 15:33:02 -0700 Subject: [PATCH 17/37] chore(shared): Allow custom package version --- packages/shared/src/loadClerkJsScript.ts | 17 +++++++++++++---- packages/shared/src/versionSelector.ts | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index d350ab53069..4e9fb10b5a7 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -22,7 +22,10 @@ type LoadClerkJsScriptOptions = Without & { domain?: string; }; -const loadClerkJsScript = async (opts: LoadClerkJsScriptOptions) => { +/* + * Loads the Clerk JS script. + */ +const loadClerkJsScript = async (opts: LoadClerkJsScriptOptions, packageVersion: string) => { const { publishableKey } = opts; if (!publishableKey) { @@ -43,7 +46,7 @@ const loadClerkJsScript = async (opts: LoadClerkJsScriptOptions) => { }); } - return loadScript(clerkJsScriptUrl(opts), { + return loadScript(clerkJsScriptUrl(opts, packageVersion), { async: true, crossOrigin: 'anonymous', beforeLoad: applyClerkJsScriptAttributes(opts), @@ -52,7 +55,10 @@ const loadClerkJsScript = async (opts: LoadClerkJsScriptOptions) => { }); }; -const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => { +/* + * Generates the Clerk JS script URL. + */ +const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions, packageVersion: string) => { const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts; if (clerkJSUrl) { @@ -69,10 +75,13 @@ const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => { } const variant = clerkJSVariant ? `${clerkJSVariant.replace(/\.+$/, '')}.` : ''; - const version = versionSelector(clerkJSVersion); + const version = versionSelector(clerkJSVersion, packageVersion); return `https://${scriptHost}/npm/@clerk/clerk-js@${version}/dist/clerk.${variant}browser.js`; }; +/* + * Builds an object of Clerk JS script attributes. + */ const buildClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => { const obj: Record = {}; diff --git a/packages/shared/src/versionSelector.ts b/packages/shared/src/versionSelector.ts index 6bc045b6e37..bc20b483ebe 100644 --- a/packages/shared/src/versionSelector.ts +++ b/packages/shared/src/versionSelector.ts @@ -8,7 +8,7 @@ * @param packageVersion - The version of `@clerk/clerk-js` that will be used if an explicit version is not provided * @returns The npm tag, version or major version to use */ -export const versionSelector = (clerkJSVersion: string | undefined, packageVersion = JS_PACKAGE_VERSION) => { +export const versionSelector = (clerkJSVersion: string | undefined, packageVersion: string) => { if (clerkJSVersion) { return clerkJSVersion; } From 84c5a462c623b9947c4989e977b3e53ef75c57f1 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 30 Jul 2024 17:19:41 -0700 Subject: [PATCH 18/37] test(shared): Handle custom package versions for versionSelector --- .../src/__tests__/loadClerkJsScript.test.ts | 2 +- .../src/__tests__/versionSelector.test.ts | 11 ++++--- packages/shared/src/loadClerkJsScript.ts | 30 +++++++++++++------ packages/shared/src/versionSelector.ts | 2 +- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/shared/src/__tests__/loadClerkJsScript.test.ts b/packages/shared/src/__tests__/loadClerkJsScript.test.ts index af2fb5d58b0..201952de5a5 100644 --- a/packages/shared/src/__tests__/loadClerkJsScript.test.ts +++ b/packages/shared/src/__tests__/loadClerkJsScript.test.ts @@ -3,7 +3,7 @@ import { loadScript } from '../loadScript'; jest.mock('../loadScript'); -describe('loadClerkJsScript()', () => { +describe('loadClerkJsScript(options)', () => { const mockPublishableKey = 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk'; beforeEach(() => { diff --git a/packages/shared/src/__tests__/versionSelector.test.ts b/packages/shared/src/__tests__/versionSelector.test.ts index 40e2812e25d..c839fe3c95f 100644 --- a/packages/shared/src/__tests__/versionSelector.test.ts +++ b/packages/shared/src/__tests__/versionSelector.test.ts @@ -6,25 +6,28 @@ describe('versionSelector', () => { expect(versionSelector('1.0.0')).toEqual('1.0.0'); }); - it('should use the major clerkJS version if there is no prerelease tag', () => { + it('should use the major version if there is no prerelease tag', () => { + const PACKAGE_VERSION = '1.0.0'; // @ts-ignore global.JS_PACKAGE_VERSION = '2.0.0'; - expect(versionSelector(undefined)).toEqual('2'); + expect(versionSelector(undefined, PACKAGE_VERSION)).toEqual('1'); }); it('should use the prerelease tag when it is not snapshot', () => { + const PACKAGE_VERSION = '1.0.0-next.0'; // @ts-ignore global.JS_PACKAGE_VERSION = '2.0.0-next.0'; - expect(versionSelector(undefined)).toEqual('next'); + expect(versionSelector(undefined, PACKAGE_VERSION)).toEqual('next'); }); it('should use the exact JS version if tag is snapshot', () => { + const PACKAGE_VERSION = '1.0.0-snapshot.0'; // @ts-ignore global.JS_PACKAGE_VERSION = '2.0.0-snapshot.0'; - expect(versionSelector(undefined)).toEqual('2.0.0-snapshot.0'); + expect(versionSelector(undefined, PACKAGE_VERSION)).toEqual('2.0.0-snapshot.0'); }); // We replaced semver with 2 custom regexes diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index 4e9fb10b5a7..0fd8f0acd75 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -22,11 +22,17 @@ type LoadClerkJsScriptOptions = Without & { domain?: string; }; -/* - * Loads the Clerk JS script. +/** + * Hotloads the Clerk JS script. + * + * @param options - The options to use when building the Clerk JS script URL. + * @param packageVersion - The version of `@clerk/clerk-js` that will be used if an explicit version is not provided. + * + * @example + * loadClerkJsScript({ publishableKey: 'pk_' }, '1.0.0'); */ -const loadClerkJsScript = async (opts: LoadClerkJsScriptOptions, packageVersion: string) => { - const { publishableKey } = opts; +const loadClerkJsScript = async (options: LoadClerkJsScriptOptions, packageVersion?: string) => { + const { publishableKey } = options; if (!publishableKey) { throw new Error(MISSING_PUBLISHABLE_KEY_ERROR); @@ -46,19 +52,25 @@ const loadClerkJsScript = async (opts: LoadClerkJsScriptOptions, packageVersion: }); } - return loadScript(clerkJsScriptUrl(opts, packageVersion), { + return loadScript(clerkJsScriptUrl(options, packageVersion), { async: true, crossOrigin: 'anonymous', - beforeLoad: applyClerkJsScriptAttributes(opts), + beforeLoad: applyClerkJsScriptAttributes(options), }).catch(() => { throw new Error(FAILED_TO_LOAD_ERROR); }); }; -/* - * Generates the Clerk JS script URL. +/** + * Generates a Clerk JS script URL. + * + * @param options - The options to use when building the Clerk JS script URL. + * @param packageVersion - The version of `@clerk/clerk-js` that will be used if an explicit version is not provided. + * + * @example + * clerkJsScriptUrl({ publishableKey: 'pk_' }, '1.0.0'); */ -const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions, packageVersion: string) => { +const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions, packageVersion?: string) => { const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts; if (clerkJSUrl) { diff --git a/packages/shared/src/versionSelector.ts b/packages/shared/src/versionSelector.ts index bc20b483ebe..6bc045b6e37 100644 --- a/packages/shared/src/versionSelector.ts +++ b/packages/shared/src/versionSelector.ts @@ -8,7 +8,7 @@ * @param packageVersion - The version of `@clerk/clerk-js` that will be used if an explicit version is not provided * @returns The npm tag, version or major version to use */ -export const versionSelector = (clerkJSVersion: string | undefined, packageVersion: string) => { +export const versionSelector = (clerkJSVersion: string | undefined, packageVersion = JS_PACKAGE_VERSION) => { if (clerkJSVersion) { return clerkJSVersion; } From 883e7341266edca36d68ea278c3384d2d474b1b1 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 30 Jul 2024 17:24:59 -0700 Subject: [PATCH 19/37] chore(shared): Improve version selector explanation --- packages/shared/src/versionSelector.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/versionSelector.ts b/packages/shared/src/versionSelector.ts index 6bc045b6e37..04134b37978 100644 --- a/packages/shared/src/versionSelector.ts +++ b/packages/shared/src/versionSelector.ts @@ -1,9 +1,9 @@ /** * This version selector is a bit complicated, so here is the flow: * 1. Use the clerkJSVersion prop on the provider - * 2. Use the exact `@clerk/clerk-js` version if tag is snapshot - * 3. Use the prerelease tag of `@clerk/clerk-js` - * 4. Fallback to the major version of `@clerk/clerk-js` + * 2. Use the exact `@clerk/clerk-js` version if it is a `@snapshot` prerelease + * 3. Use the prerelease tag of `@clerk/clerk-js` or the package version provided + * 4. Fallback to the major version of `@clerk/clerk-js` or the package version provided * @param clerkJSVersion - The optional clerkJSVersion prop on the provider * @param packageVersion - The version of `@clerk/clerk-js` that will be used if an explicit version is not provided * @returns The npm tag, version or major version to use From 7605a54aaefaa00e954f1433c425007aa86ac964 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 31 Jul 2024 16:53:37 -0700 Subject: [PATCH 20/37] chore(astro): Use shared clerk-js hotload script --- .../src/internal/create-clerk-instance.ts | 12 +++--- packages/astro/src/internal/types.ts | 2 +- .../src/server/build-clerk-hotload-script.ts | 37 +------------------ packages/shared/src/loadClerkJsScript.ts | 24 +++++++----- 4 files changed, 21 insertions(+), 54 deletions(-) diff --git a/packages/astro/src/internal/create-clerk-instance.ts b/packages/astro/src/internal/create-clerk-instance.ts index 8e449ceabc3..2c7f1846ee1 100644 --- a/packages/astro/src/internal/create-clerk-instance.ts +++ b/packages/astro/src/internal/create-clerk-instance.ts @@ -1,11 +1,11 @@ +import { loadClerkJsScript } from '@clerk/shared/loadClerkJsScript'; import type { ClerkOptions } from '@clerk/types'; import { $clerk, $csrState } from '../stores/internal'; -import type { AstroClerkIntegrationParams, AstroClerkUpdateOptions } from '../types'; +import type { AstroClerkCreateInstanceParams, AstroClerkUpdateOptions } from '../types'; import { invokeClerkAstroJSFunctions } from './invoke-clerk-astro-js-functions'; import { mountAllClerkAstroJSComponents } from './mount-clerk-astro-js-components'; import { runOnce } from './run-once'; -import { waitForClerkScript } from './utils/loadClerkJSScript'; let initOptions: ClerkOptions | undefined; @@ -35,10 +35,10 @@ function createNavigationHandler( */ const createClerkInstance = runOnce(createClerkInstanceInternal); -async function createClerkInstanceInternal(options?: AstroClerkIntegrationParams) { +async function createClerkInstanceInternal(options: AstroClerkCreateInstanceParams) { let clerkJSInstance = window.Clerk; if (!clerkJSInstance) { - await waitForClerkScript(); + await loadClerkJsScript(options); if (!window.Clerk) { throw new Error('Failed to download latest ClerkJS. Contact support@clerk.com.'); @@ -47,7 +47,6 @@ async function createClerkInstanceInternal(options?: AstroClerkIntegrationParams } if (!$clerk.get()) { - // @ts-ignore $clerk.set(clerkJSInstance); } @@ -57,8 +56,7 @@ async function createClerkInstanceInternal(options?: AstroClerkIntegrationParams routerReplace: createNavigationHandler(window.history.replaceState.bind(window.history)), }; - // TODO: Update Clerk type from @clerk/types to include this method - return (clerkJSInstance as any) + return clerkJSInstance .load(initOptions) .then(() => { $csrState.setKey('isLoaded', true); diff --git a/packages/astro/src/internal/types.ts b/packages/astro/src/internal/types.ts index 9128cff0501..34cf8add0f5 100644 --- a/packages/astro/src/internal/types.ts +++ b/packages/astro/src/internal/types.ts @@ -1,5 +1,5 @@ import type { AstroClerkCreateInstanceParams } from '../types'; -type CreateClerkInstanceInternalFn = (options?: AstroClerkCreateInstanceParams) => Promise; +type CreateClerkInstanceInternalFn = (options: AstroClerkCreateInstanceParams) => Promise; export type { CreateClerkInstanceInternalFn }; diff --git a/packages/astro/src/server/build-clerk-hotload-script.ts b/packages/astro/src/server/build-clerk-hotload-script.ts index 0bc7f5b8ea7..87adac68779 100644 --- a/packages/astro/src/server/build-clerk-hotload-script.ts +++ b/packages/astro/src/server/build-clerk-hotload-script.ts @@ -1,43 +1,8 @@ -import { createDevOrStagingUrlCache, parsePublishableKey } from '@clerk/shared/keys'; -import { isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/shared/proxy'; -import { addClerkPrefix } from '@clerk/shared/url'; +import { clerkJsScriptUrl } from '@clerk/shared/loadClerkJsScript'; import type { APIContext } from 'astro'; -import { versionSelector } from '../internal/utils/versionSelector'; import { getSafeEnv } from './get-safe-env'; -const { isDevOrStagingUrl } = createDevOrStagingUrlCache(); - -type BuildClerkJsScriptOptions = { - proxyUrl: string; - domain: string; - clerkJSUrl?: string; - clerkJSVariant?: 'headless' | ''; - clerkJSVersion?: string; - publishableKey: string; -}; - -const clerkJsScriptUrl = (opts: BuildClerkJsScriptOptions) => { - const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts; - - if (clerkJSUrl) { - return clerkJSUrl; - } - - let scriptHost = ''; - if (!!proxyUrl && isValidProxyUrl(proxyUrl)) { - scriptHost = proxyUrlToAbsoluteURL(proxyUrl).replace(/http(s)?:\/\//, ''); - } else if (domain && !isDevOrStagingUrl(parsePublishableKey(publishableKey)?.frontendApi || '')) { - scriptHost = addClerkPrefix(domain); - } else { - scriptHost = parsePublishableKey(publishableKey)?.frontendApi || ''; - } - - const variant = clerkJSVariant ? `${clerkJSVariant.replace(/\.+$/, '')}.` : ''; - const version = versionSelector(clerkJSVersion); - return `https://${scriptHost}/npm/@clerk/clerk-js@${version}/dist/clerk.${variant}browser.js`; -}; - function buildClerkHotloadScript(locals: APIContext['locals']) { const publishableKey = getSafeEnv(locals).pk!; const proxyUrl = getSafeEnv(locals).proxyUrl!; diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index 0fd8f0acd75..50cc4c9d703 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -20,18 +20,23 @@ type LoadClerkJsScriptOptions = Without & { sdkMetadata?: SDKMetadata; proxyUrl?: string; domain?: string; + /** + * @internal + * The version of `@clerk/clerk-js` that will be used if an explicit version + * is not provided. Used for prerelease tags. + */ + _packageVersion?: string; }; /** * Hotloads the Clerk JS script. * * @param options - The options to use when building the Clerk JS script URL. - * @param packageVersion - The version of `@clerk/clerk-js` that will be used if an explicit version is not provided. * * @example - * loadClerkJsScript({ publishableKey: 'pk_' }, '1.0.0'); + * loadClerkJsScript({ publishableKey: 'pk_' }); */ -const loadClerkJsScript = async (options: LoadClerkJsScriptOptions, packageVersion?: string) => { +const loadClerkJsScript = async (options: LoadClerkJsScriptOptions) => { const { publishableKey } = options; if (!publishableKey) { @@ -52,7 +57,7 @@ const loadClerkJsScript = async (options: LoadClerkJsScriptOptions, packageVersi }); } - return loadScript(clerkJsScriptUrl(options, packageVersion), { + return loadScript(clerkJsScriptUrl(options), { async: true, crossOrigin: 'anonymous', beforeLoad: applyClerkJsScriptAttributes(options), @@ -65,13 +70,12 @@ const loadClerkJsScript = async (options: LoadClerkJsScriptOptions, packageVersi * Generates a Clerk JS script URL. * * @param options - The options to use when building the Clerk JS script URL. - * @param packageVersion - The version of `@clerk/clerk-js` that will be used if an explicit version is not provided. * * @example - * clerkJsScriptUrl({ publishableKey: 'pk_' }, '1.0.0'); + * clerkJsScriptUrl({ publishableKey: 'pk_' }); */ -const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions, packageVersion?: string) => { - const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts; +const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => { + const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey, _packageVersion } = opts; if (clerkJSUrl) { return clerkJSUrl; @@ -87,11 +91,11 @@ const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions, packageVersion?: strin } const variant = clerkJSVariant ? `${clerkJSVariant.replace(/\.+$/, '')}.` : ''; - const version = versionSelector(clerkJSVersion, packageVersion); + const version = versionSelector(clerkJSVersion, _packageVersion); return `https://${scriptHost}/npm/@clerk/clerk-js@${version}/dist/clerk.${variant}browser.js`; }; -/* +/** * Builds an object of Clerk JS script attributes. */ const buildClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => { From c9ed36ef88484b6543747d6b19558a7b71cb45ba Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 31 Jul 2024 16:59:23 -0700 Subject: [PATCH 21/37] chore(shared): Update internal package version property name --- packages/shared/src/loadClerkJsScript.ts | 9 +++++---- packages/shared/src/versionSelector.ts | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index 50cc4c9d703..86f2e4c3cdf 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -23,9 +23,9 @@ type LoadClerkJsScriptOptions = Without & { /** * @internal * The version of `@clerk/clerk-js` that will be used if an explicit version - * is not provided. Used for prerelease tags. + * is not provided. For `@clerk/clerk-react`, this will be the PACKAGE_VERSION. */ - _packageVersion?: string; + __internal_packageVersion?: string; }; /** @@ -75,7 +75,8 @@ const loadClerkJsScript = async (options: LoadClerkJsScriptOptions) => { * clerkJsScriptUrl({ publishableKey: 'pk_' }); */ const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => { - const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey, _packageVersion } = opts; + const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey, __internal_packageVersion } = + opts; if (clerkJSUrl) { return clerkJSUrl; @@ -91,7 +92,7 @@ const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => { } const variant = clerkJSVariant ? `${clerkJSVariant.replace(/\.+$/, '')}.` : ''; - const version = versionSelector(clerkJSVersion, _packageVersion); + const version = versionSelector(clerkJSVersion, __internal_packageVersion); return `https://${scriptHost}/npm/@clerk/clerk-js@${version}/dist/clerk.${variant}browser.js`; }; diff --git a/packages/shared/src/versionSelector.ts b/packages/shared/src/versionSelector.ts index 04134b37978..c8f65bff413 100644 --- a/packages/shared/src/versionSelector.ts +++ b/packages/shared/src/versionSelector.ts @@ -2,8 +2,8 @@ * This version selector is a bit complicated, so here is the flow: * 1. Use the clerkJSVersion prop on the provider * 2. Use the exact `@clerk/clerk-js` version if it is a `@snapshot` prerelease - * 3. Use the prerelease tag of `@clerk/clerk-js` or the package version provided - * 4. Fallback to the major version of `@clerk/clerk-js` or the package version provided + * 3. Use the prerelease tag of `@clerk/clerk-js` or the packageVersion provided + * 4. Fallback to the major version of `@clerk/clerk-js` or the packageVersion provided * @param clerkJSVersion - The optional clerkJSVersion prop on the provider * @param packageVersion - The version of `@clerk/clerk-js` that will be used if an explicit version is not provided * @returns The npm tag, version or major version to use From a76dd7bbdee77fed114e047b123da7a6cbfdb696 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 31 Jul 2024 17:07:09 -0700 Subject: [PATCH 22/37] chore(astro): Remove unused file --- packages/astro/src/internal/utils/versionSelector.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 packages/astro/src/internal/utils/versionSelector.ts diff --git a/packages/astro/src/internal/utils/versionSelector.ts b/packages/astro/src/internal/utils/versionSelector.ts deleted file mode 100644 index 12b8d4c2fdd..00000000000 --- a/packages/astro/src/internal/utils/versionSelector.ts +++ /dev/null @@ -1,9 +0,0 @@ -const HARDCODED_LATEST_CLERK_JS_VERSION = '5'; - -export const versionSelector = (clerkJSVersion: string | undefined): string => { - if (clerkJSVersion) { - return clerkJSVersion; - } - - return HARDCODED_LATEST_CLERK_JS_VERSION; -}; From 9e6c6de46652d02a02606f4c6f10d9676ce307cb Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 31 Jul 2024 17:50:15 -0700 Subject: [PATCH 23/37] chore(astro): Clean up param names --- packages/shared/src/loadClerkJsScript.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index 86f2e4c3cdf..d077c5aaea9 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -31,13 +31,13 @@ type LoadClerkJsScriptOptions = Without & { /** * Hotloads the Clerk JS script. * - * @param options - The options to use when building the Clerk JS script URL. + * @param opts - The options to use when building the Clerk JS script URL. * * @example * loadClerkJsScript({ publishableKey: 'pk_' }); */ -const loadClerkJsScript = async (options: LoadClerkJsScriptOptions) => { - const { publishableKey } = options; +const loadClerkJsScript = async (opts: LoadClerkJsScriptOptions) => { + const { publishableKey } = opts; if (!publishableKey) { throw new Error(MISSING_PUBLISHABLE_KEY_ERROR); @@ -57,10 +57,10 @@ const loadClerkJsScript = async (options: LoadClerkJsScriptOptions) => { }); } - return loadScript(clerkJsScriptUrl(options), { + return loadScript(clerkJsScriptUrl(opts), { async: true, crossOrigin: 'anonymous', - beforeLoad: applyClerkJsScriptAttributes(options), + beforeLoad: applyClerkJsScriptAttributes(opts), }).catch(() => { throw new Error(FAILED_TO_LOAD_ERROR); }); @@ -69,7 +69,7 @@ const loadClerkJsScript = async (options: LoadClerkJsScriptOptions) => { /** * Generates a Clerk JS script URL. * - * @param options - The options to use when building the Clerk JS script URL. + * @param opts - The options to use when building the Clerk JS script URL. * * @example * clerkJsScriptUrl({ publishableKey: 'pk_' }); From 396936491e672faac7d5d5dac6532d3dfcdd5103 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 5 Aug 2024 09:04:30 -0700 Subject: [PATCH 24/37] chore(clerk-react): Use shared loadClerkJsScript when hotloading Clerk js --- packages/react/src/internal.ts | 2 +- packages/react/src/isomorphicClerk.ts | 3 +- .../utils/__tests__/versionSelector.test.ts | 56 ------------------- packages/react/src/utils/index.ts | 1 - packages/react/src/utils/versionSelector.ts | 34 ----------- 5 files changed, 3 insertions(+), 93 deletions(-) delete mode 100644 packages/react/src/utils/__tests__/versionSelector.test.ts delete mode 100644 packages/react/src/utils/versionSelector.ts diff --git a/packages/react/src/internal.ts b/packages/react/src/internal.ts index ab014b77df5..a5874a389e4 100644 --- a/packages/react/src/internal.ts +++ b/packages/react/src/internal.ts @@ -2,4 +2,4 @@ export { setErrorThrowerOptions } from './errors/errorThrower'; export { MultisessionAppSupport } from './components/controlComponents'; export { useRoutingProps } from './hooks/useRoutingProps'; -export { clerkJsScriptUrl, buildClerkJsScriptAttributes } from './utils/loadClerkJsScript'; +export { clerkJsScriptUrl, buildClerkJsScriptAttributes } from '@clerk/shared/loadClerkJsScript'; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 09071059c7f..4b21a188f2c 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -1,5 +1,6 @@ import { inBrowser } from '@clerk/shared/browser'; import { handleValueOrFn } from '@clerk/shared/handleValueOrFn'; +import { loadClerkJsScript } from '@clerk/shared/loadClerkJsScript'; import type { TelemetryCollector } from '@clerk/shared/telemetry'; import type { ActiveSessionResource, @@ -49,7 +50,7 @@ import type { HeadlessBrowserClerkConstructor, IsomorphicClerkOptions, } from './types'; -import { isConstructor, loadClerkJsScript } from './utils'; +import { isConstructor } from './utils'; const SDK_METADATA = { name: PACKAGE_NAME, diff --git a/packages/react/src/utils/__tests__/versionSelector.test.ts b/packages/react/src/utils/__tests__/versionSelector.test.ts deleted file mode 100644 index d600b325218..00000000000 --- a/packages/react/src/utils/__tests__/versionSelector.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import { versionSelector } from '../versionSelector'; - -describe('versionSelector', () => { - it('should return the clerkJSVersion if it is provided', () => { - expect(versionSelector('1.0.0')).toEqual('1.0.0'); - }); - - it('should use the major version if there is no prerelease tag', () => { - // @ts-ignore - global.PACKAGE_VERSION = '1.0.0'; - // @ts-ignore - global.JS_PACKAGE_VERSION = '2.0.0'; - - expect(versionSelector(undefined)).toEqual('1'); - }); - - it('should use the prerelease tag when it is not snapshot', () => { - // @ts-ignore - global.PACKAGE_VERSION = '1.0.0-next.0'; - // @ts-ignore - global.JS_PACKAGE_VERSION = '2.0.0-next.0'; - - expect(versionSelector(undefined)).toEqual('next'); - }); - - it('should use the exact JS version if tag is snapshot', () => { - // @ts-ignore - global.PACKAGE_VERSION = '1.0.0-snapshot.0'; - // @ts-ignore - global.JS_PACKAGE_VERSION = '2.0.0-snapshot.0'; - - expect(versionSelector(undefined)).toEqual('2.0.0-snapshot.0'); - }); - - // We replaced semver with 2 custom regexes - // so we're testing the same cases as semver tests - // https://github.com/npm/node-semver/blob/main/test/functions/prerelease.js - // https://github.com/npm/node-semver/blob/main/test/functions/major.js - test.each([ - ['1.2.3', 1], - [' 1.2.3 ', 1], - [' 2.2.3-4 ', 4], - [' 3.2.3-pre ', 'pre'], - ['v5.2.3', 5], - [' v8.2.3 ', 8], - ['\t13.2.3', 13], - ['1.2.2-alpha.1', 'alpha'], - ['1.2.2-beta.1', 'beta'], - ['1.2.2-rc.1', 'rc'], - ['1.2.2', 1], - ['1.2.2-pre', 'pre'], - ])('versionSelector(%s) should return %i', (version, expected) => { - expect(versionSelector(undefined, version)).toEqual(expected.toString()); - }); -}); diff --git a/packages/react/src/utils/index.ts b/packages/react/src/utils/index.ts index d776edbffe6..b113a6f5b76 100644 --- a/packages/react/src/utils/index.ts +++ b/packages/react/src/utils/index.ts @@ -1,6 +1,5 @@ export * from './childrenUtils'; export * from './isConstructor'; -export { loadClerkJsScript } from './loadClerkJsScript'; export * from './useMaxAllowedInstancesGuard'; export * from './useCustomElementPortal'; export * from './useCustomPages'; diff --git a/packages/react/src/utils/versionSelector.ts b/packages/react/src/utils/versionSelector.ts deleted file mode 100644 index 6a07495706c..00000000000 --- a/packages/react/src/utils/versionSelector.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * This version selector is a bit complicated, so here is the flow: - * 1. Use the clerkJSVersion prop on the provider - * 2. Use the exact `@clerk/clerk-js` version if it is a `@snapshot` prerelease for `@clerk/clerk-react` - * 3. Use the prerelease tag of `@clerk/clerk-react` - * 4. Fallback to the major version of `@clerk/clerk-react` - * @param clerkJSVersion - The optional clerkJSVersion prop on the provider - * @param packageVersion - The version of `@clerk/clerk-react` that will be used if an explicit version is not provided - * @returns The npm tag, version or major version to use - */ -export const versionSelector = (clerkJSVersion: string | undefined, packageVersion = PACKAGE_VERSION) => { - if (clerkJSVersion) { - return clerkJSVersion; - } - - const prereleaseTag = getPrereleaseTag(packageVersion); - if (prereleaseTag) { - if (prereleaseTag === 'snapshot') { - return JS_PACKAGE_VERSION; - } - - return prereleaseTag; - } - - return getMajorVersion(packageVersion); -}; - -const getPrereleaseTag = (packageVersion: string) => - packageVersion - .trim() - .replace(/^v/, '') - .match(/-(.+?)(\.|$)/)?.[1]; - -const getMajorVersion = (packageVersion: string) => packageVersion.trim().replace(/^v/, '').split('.')[0]; From 06ab32e0ce027b86297d2230ee4de93277183684 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 5 Aug 2024 09:05:42 -0700 Subject: [PATCH 25/37] chore(clerk-react,astro,shared): Update changeset --- .changeset/nasty-baboons-cheer.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/nasty-baboons-cheer.md b/.changeset/nasty-baboons-cheer.md index 9d2f49a215c..0859b9a4fd1 100644 --- a/.changeset/nasty-baboons-cheer.md +++ b/.changeset/nasty-baboons-cheer.md @@ -1,6 +1,7 @@ --- "@clerk/astro": patch "@clerk/shared": patch +"@clerk/clerk-react": patch --- Introduce functions that can be reused across front-end SDKs From a6a3a2c98b90edcf32d9fd73e012c4a9ed6a39a1 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 5 Aug 2024 10:07:00 -0700 Subject: [PATCH 26/37] chore(shared): Make options optional in loadClerkJsScript and throw if clerk-js script attribute is undefined --- .../astro/src/internal/create-clerk-instance.ts | 2 +- packages/astro/src/internal/types.ts | 2 +- .../src/internal/utils/loadClerkJSScript.ts | 17 ----------------- .../src/server/build-clerk-hotload-script.ts | 2 +- packages/shared/src/loadClerkJsScript.ts | 12 +++++------- 5 files changed, 8 insertions(+), 27 deletions(-) delete mode 100644 packages/astro/src/internal/utils/loadClerkJSScript.ts diff --git a/packages/astro/src/internal/create-clerk-instance.ts b/packages/astro/src/internal/create-clerk-instance.ts index 2c7f1846ee1..78dfbee04c6 100644 --- a/packages/astro/src/internal/create-clerk-instance.ts +++ b/packages/astro/src/internal/create-clerk-instance.ts @@ -35,7 +35,7 @@ function createNavigationHandler( */ const createClerkInstance = runOnce(createClerkInstanceInternal); -async function createClerkInstanceInternal(options: AstroClerkCreateInstanceParams) { +async function createClerkInstanceInternal(options?: AstroClerkCreateInstanceParams) { let clerkJSInstance = window.Clerk; if (!clerkJSInstance) { await loadClerkJsScript(options); diff --git a/packages/astro/src/internal/types.ts b/packages/astro/src/internal/types.ts index 34cf8add0f5..9128cff0501 100644 --- a/packages/astro/src/internal/types.ts +++ b/packages/astro/src/internal/types.ts @@ -1,5 +1,5 @@ import type { AstroClerkCreateInstanceParams } from '../types'; -type CreateClerkInstanceInternalFn = (options: AstroClerkCreateInstanceParams) => Promise; +type CreateClerkInstanceInternalFn = (options?: AstroClerkCreateInstanceParams) => Promise; export type { CreateClerkInstanceInternalFn }; diff --git a/packages/astro/src/internal/utils/loadClerkJSScript.ts b/packages/astro/src/internal/utils/loadClerkJSScript.ts deleted file mode 100644 index 6d250441339..00000000000 --- a/packages/astro/src/internal/utils/loadClerkJSScript.ts +++ /dev/null @@ -1,17 +0,0 @@ -const FAILED_TO_FIND_CLERK_SCRIPT = 'Clerk: Failed find clerk-js script'; - -// TODO-SHARED: Something similar exists inside clerk-react -export const waitForClerkScript = () => { - return new Promise((resolve, reject) => { - const script = document.querySelector('script[data-clerk-script]'); - - if (!script) { - return reject(FAILED_TO_FIND_CLERK_SCRIPT); - } - - script.addEventListener('load', () => { - script.remove(); - resolve(script); - }); - }); -}; diff --git a/packages/astro/src/server/build-clerk-hotload-script.ts b/packages/astro/src/server/build-clerk-hotload-script.ts index 87adac68779..d135dd3bcb2 100644 --- a/packages/astro/src/server/build-clerk-hotload-script.ts +++ b/packages/astro/src/server/build-clerk-hotload-script.ts @@ -17,7 +17,7 @@ function buildClerkHotloadScript(locals: APIContext['locals']) { }); return `