From b1bfa608c8381de4b15efc2798cc56fc49c76944 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 11:48:18 -0700 Subject: [PATCH 01/14] 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 a074e7d3427..90bf2a8f534 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 fcae45e371264f91a60ee91bca73126a64d2407c Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 11:55:45 -0700 Subject: [PATCH 02/14] 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 e60b635464687954e2dbdec029cec577dffd1fd3 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 12:42:42 -0700 Subject: [PATCH 03/14] 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 a05b2c8e1beaa0d8228a0d6ca33c77fbecad2de0 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 12:49:42 -0700 Subject: [PATCH 04/14] 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 542da2df2d0cb21fbf74a6fad28d025da199271e Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 13:43:58 -0700 Subject: [PATCH 05/14] 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 90bf2a8f534..5b9237017e4 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 696b2219e467abb70e200f666c1eafe3a9d57069 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 9 Jul 2024 19:07:35 -0700 Subject: [PATCH 06/14] 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 401cc897733e317c66d526e8d53085c9e6da883e Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 10 Jul 2024 12:07:48 -0700 Subject: [PATCH 07/14] 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 69fa64476fc4055b9b66f5e921d22d5764d4728f Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 10 Jul 2024 14:13:16 -0700 Subject: [PATCH 08/14] 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 f159c5e390153e4050c4fff9d1221f52d5cd435c Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 10 Jul 2024 15:40:30 -0700 Subject: [PATCH 10/14] chore(astro): Use shared clerk-js hotload script --- packages/astro/src/client/index.ts | 5 ++- .../internal/merge-env-vars-with-params.ts | 18 ++++++--- .../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 | 6 --- 6 files changed, 15 insertions(+), 78 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/client/index.ts b/packages/astro/src/client/index.ts index ffe5b8c612b..46ef370230f 100644 --- a/packages/astro/src/client/index.ts +++ b/packages/astro/src/client/index.ts @@ -1,4 +1,5 @@ -import { waitForClerkScript } from '../internal/utils/loadClerkJSScript'; +import { loadClerkJsScript } from '@clerk/shared'; + import { $clerk, $csrState } from '../stores/internal'; import type { AstroClerkIntegrationParams, AstroClerkUpdateOptions } from '../types'; import { mountAllClerkAstroJSComponents } from './mount-clerk-astro-js-components'; @@ -14,7 +15,7 @@ export const createClerkInstance = runOnce(createClerkInstanceInternal); export async function createClerkInstanceInternal(options?: AstroClerkIntegrationParams) { let clerkJSInstance = window.Clerk; if (!clerkJSInstance) { - await waitForClerkScript(); + await loadClerkJsScript(options as any); if (!window.Clerk) { throw new Error('Failed to download latest ClerkJS. Contact support@clerk.com.'); 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 b8aebfa02fc..fb82b5d54ce 100644 --- a/packages/astro/src/internal/merge-env-vars-with-params.ts +++ b/packages/astro/src/internal/merge-env-vars-with-params.ts @@ -15,12 +15,18 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish } = params || {}; return { - signInUrl: paramSignIn || import.meta.env.PUBLIC_ASTRO_APP_CLERK_SIGN_IN_URL, - signUpUrl: paramSignUp || import.meta.env.PUBLIC_ASTRO_APP_CLERK_SIGN_UP_URL, - isSatellite: paramSatellite || import.meta.env.PUBLIC_ASTRO_APP_CLERK_IS_SATELLITE, - proxyUrl: paramProxy || import.meta.env.PUBLIC_ASTRO_APP_CLERK_PROXY_URL, - domain: paramDomain || import.meta.env.PUBLIC_ASTRO_APP_CLERK_DOMAIN, - publishableKey: paramPublishableKey || import.meta.env.PUBLIC_ASTRO_APP_CLERK_PUBLISHABLE_KEY || '', + // eslint-disable-next-line turbo/no-undeclared-env-vars + signInUrl: paramSignIn || import.meta.env.PUBLIC_CLERK_SIGN_IN_URL, + // eslint-disable-next-line turbo/no-undeclared-env-vars + signUpUrl: paramSignUp || import.meta.env.PUBLIC_CLERK_SIGN_UP_URL, + // eslint-disable-next-line turbo/no-undeclared-env-vars + isSatellite: paramSatellite || import.meta.env.PUBLIC_CLERK_IS_SATELLITE, + // eslint-disable-next-line turbo/no-undeclared-env-vars + proxyUrl: paramProxy || import.meta.env.PUBLIC_CLERK_PROXY_URL, + // eslint-disable-next-line turbo/no-undeclared-env-vars + domain: paramDomain || import.meta.env.PUBLIC_CLERK_DOMAIN, + // eslint-disable-next-line turbo/no-undeclared-env-vars + publishableKey: paramPublishableKey || import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY || '', ...rest, }; }; 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 be08f6642af..8e62ca47423 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'; @@ -263,7 +262,6 @@ async function decorateRequest( const clerkSafeEnvVariables = encoder.encode( `\n`, ); - const hotloadScript = encoder.encode(buildClerkHotloadScript(locals)); const stream = res.body!.pipeThrough( new TransformStream({ @@ -279,10 +277,6 @@ async function decorateRequest( controller.enqueue(clerkAstroData); controller.enqueue(clerkSafeEnvVariables); - if (__HOTLOAD__) { - controller.enqueue(hotloadScript); - } - controller.enqueue(closingHeadTag); controller.enqueue(chunk.slice(index + closingHeadTag.length)); } else { From 6af9ea55d172220599cae6ee378cb0182b9beac4 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 10 Jul 2024 16:09:17 -0700 Subject: [PATCH 11/14] chore(astro): Clean up type ignores --- packages/astro/src/client/bundled.ts | 2 -- packages/astro/src/client/index.ts | 11 +++++++---- .../astro/src/internal/merge-env-vars-with-params.ts | 2 ++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/astro/src/client/bundled.ts b/packages/astro/src/client/bundled.ts index b2a7b9380d9..151505bac0f 100644 --- a/packages/astro/src/client/bundled.ts +++ b/packages/astro/src/client/bundled.ts @@ -17,9 +17,7 @@ export function createClerkInstanceInternal(options?: AstroClerkCreateInstancePa let clerkJSInstance = window.Clerk as unknown as Clerk; if (!clerkJSInstance) { clerkJSInstance = new Clerk(options!.publishableKey); - // @ts-ignore $clerk.set(clerkJSInstance); - // @ts-ignore window.Clerk = clerkJSInstance; } diff --git a/packages/astro/src/client/index.ts b/packages/astro/src/client/index.ts index 46ef370230f..306864db850 100644 --- a/packages/astro/src/client/index.ts +++ b/packages/astro/src/client/index.ts @@ -7,6 +7,8 @@ import { runOnce } from './run-once'; let initOptions: AstroClerkIntegrationParams | undefined; +const HARDCODED_LATEST_CLERK_JS_VERSION = '5'; + /** * Prevents firing clerk.load multiple times */ @@ -15,7 +17,10 @@ export const createClerkInstance = runOnce(createClerkInstanceInternal); export async function createClerkInstanceInternal(options?: AstroClerkIntegrationParams) { let clerkJSInstance = window.Clerk; if (!clerkJSInstance) { - await loadClerkJsScript(options as any); + await loadClerkJsScript({ + clerkJSVersion: HARDCODED_LATEST_CLERK_JS_VERSION, + ...(options as any), + }); if (!window.Clerk) { throw new Error('Failed to download latest ClerkJS. Contact support@clerk.com.'); @@ -24,13 +29,11 @@ export async function createClerkInstanceInternal(options?: AstroClerkIntegratio } if (!$clerk.get()) { - // @ts-ignore $clerk.set(clerkJSInstance); } initOptions = options; - // TODO: Update Clerk type from @clerk/types to include this method - return (clerkJSInstance as any) + return clerkJSInstance .load(options) .then(() => { $csrState.setKey('isLoaded', true); 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 fb82b5d54ce..69768fca35f 100644 --- a/packages/astro/src/internal/merge-env-vars-with-params.ts +++ b/packages/astro/src/internal/merge-env-vars-with-params.ts @@ -14,6 +14,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 { // eslint-disable-next-line turbo/no-undeclared-env-vars signInUrl: paramSignIn || import.meta.env.PUBLIC_CLERK_SIGN_IN_URL, From df670e116e61d2dfcd184eaa8a8cf3d4f40ffd38 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Wed, 10 Jul 2024 18:55:29 -0700 Subject: [PATCH 12/14] 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 1cd7611a51d39830dc2cdd7c578a803ff45477da Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Thu, 11 Jul 2024 07:20:33 -0700 Subject: [PATCH 13/14] chore(astro): Refine script import path Co-authored-by: Lennart --- packages/astro/src/client/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/client/index.ts b/packages/astro/src/client/index.ts index 306864db850..67f030f1311 100644 --- a/packages/astro/src/client/index.ts +++ b/packages/astro/src/client/index.ts @@ -1,4 +1,4 @@ -import { loadClerkJsScript } from '@clerk/shared'; +import { loadClerkJsScript } from '@clerk/shared/loadClerkJsScript'; import { $clerk, $csrState } from '../stores/internal'; import type { AstroClerkIntegrationParams, AstroClerkUpdateOptions } from '../types'; From 1235e8d345d5f9926c2fed0c39179f40d2724c6c Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 11 Jul 2024 09:48:21 -0700 Subject: [PATCH 14/14] 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 5b9237017e4..871ab22abe5 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}`, }, };