Skip to content

Commit fe2ebca

Browse files
committed
feat(clerk-js): Add js prop for bundling clerk-js with applications
Add a `js` prop to ClerkProvider (React, Next.js, Vue, Astro) that allows bundling @clerk/clerk-js with the application instead of loading from CDN, mirroring the established `ui` prop pattern from @clerk/ui. - Add branded Js type and bundled/server/entry exports to @clerk/clerk-js - Add js prop to ClerkOptions in @clerk/shared - Support bundled js.ClerkJS in isomorphicClerk with CDN skip - Add Next.js RSC resolution with cached promise pattern - Add Vue plugin support for bundled ClerkJS constructor - Add Astro integration support with PUBLIC_CLERK_SKIP_JS_CDN env var - Add export snapshot tests for @clerk/clerk-js
1 parent 2ff841c commit fe2ebca

24 files changed

Lines changed: 171 additions & 66 deletions

.changeset/shiny-owls-dance.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
'@clerk/clerk-js': minor
23
'@clerk/ui': minor
34
'@clerk/react': minor
45
'@clerk/nextjs': minor
@@ -8,4 +9,4 @@
89
'@clerk/shared': minor
910
---
1011

11-
Add `ui` prop to `ClerkProvider` for passing `@clerk/ui`
12+
Add `ui` and `js` props to `ClerkProvider` for passing `@clerk/ui` and `@clerk/clerk-js`

packages/astro/src/env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface InternalEnv {
88
readonly PUBLIC_CLERK_UI_URL?: string;
99
readonly PUBLIC_CLERK_UI_VERSION?: string;
1010
readonly PUBLIC_CLERK_PREFETCH_UI?: string;
11+
readonly PUBLIC_CLERK_SKIP_JS_CDN?: string;
1112
readonly CLERK_API_KEY?: string;
1213
readonly CLERK_API_URL?: string;
1314
readonly CLERK_API_VERSION?: string;

packages/astro/src/integration/create-integration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
2424
const clerkUIVersion = (params as any)?.clerkUIVersion as string | undefined;
2525
const prefetchUI = (params as any)?.prefetchUI as boolean | undefined;
2626
const hasUI = !!(params as any)?.ui;
27+
const hasJS = !!(params as any)?.js;
2728

2829
return {
2930
name: '@clerk/astro/integration',
@@ -64,6 +65,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
6465
prefetchUI === false || hasUI ? 'false' : undefined,
6566
'PUBLIC_CLERK_PREFETCH_UI',
6667
),
68+
...buildEnvVarFromOption(hasJS ? 'true' : undefined, 'PUBLIC_CLERK_SKIP_JS_CDN'),
6769
},
6870

6971
ssr: {
@@ -173,6 +175,7 @@ function createClerkEnvSchema() {
173175
PUBLIC_CLERK_JS_VERSION: envField.string({ context: 'client', access: 'public', optional: true }),
174176
PUBLIC_CLERK_UI_VERSION: envField.string({ context: 'client', access: 'public', optional: true }),
175177
PUBLIC_CLERK_PREFETCH_UI: envField.string({ context: 'client', access: 'public', optional: true }),
178+
PUBLIC_CLERK_SKIP_JS_CDN: envField.string({ context: 'client', access: 'public', optional: true }),
176179
PUBLIC_CLERK_UI_URL: envField.string({ context: 'client', access: 'public', optional: true, url: true }),
177180
PUBLIC_CLERK_TELEMETRY_DISABLED: envField.boolean({ context: 'client', access: 'public', optional: true }),
178181
PUBLIC_CLERK_TELEMETRY_DEBUG: envField.boolean({ context: 'client', access: 'public', optional: true }),

packages/astro/src/internal/create-clerk-instance.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
loadClerkUIScript,
44
setClerkJSLoadingErrorPackageName,
55
} from '@clerk/shared/loadClerkJsScript';
6-
import type { ClerkOptions } from '@clerk/shared/types';
6+
import type { BrowserClerkConstructor, ClerkOptions } from '@clerk/shared/types';
77
import type { ClerkUIConstructor } from '@clerk/shared/ui';
88
import type { Ui } from '@clerk/ui/internal';
99

@@ -102,9 +102,18 @@ function updateClerkOptions<TUi extends Ui = Ui>(options: AstroClerkUpdateOption
102102

103103
/**
104104
* Loads clerk-js script if not already loaded.
105+
* Uses bundled ClerkJS constructor when js.ClerkJS is present.
105106
* Returns early if window.Clerk already exists.
106107
*/
107108
async function getClerkJsEntryChunk<TUi extends Ui = Ui>(options?: AstroClerkCreateInstanceParams<TUi>): Promise<void> {
109+
const jsProp = options as { js?: { ClerkJS?: BrowserClerkConstructor } } | undefined;
110+
if (jsProp?.js?.ClerkJS) {
111+
window.Clerk = new jsProp.js.ClerkJS(options!.publishableKey, {
112+
proxyUrl: options?.proxyUrl as string,
113+
domain: options?.domain as string,
114+
}) as any;
115+
return;
116+
}
108117
await loadClerkJSScript(options);
109118
}
110119

packages/astro/src/server/build-clerk-hotload-script.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,30 @@ function buildClerkHotloadScript(locals: APIContext['locals']) {
1212
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1313
const domain = env.domain!;
1414

15+
// Skip ClerkJS CDN script when js prop is bundled
16+
if (env.skipJsCdn) {
17+
if (env.prefetchUI === false) {
18+
return '\n';
19+
}
20+
21+
const clerkUIScriptSrc = clerkUIScriptUrl({
22+
clerkUIUrl: env.clerkUIUrl,
23+
clerkUIVersion: env.clerkUIVersion,
24+
domain,
25+
proxyUrl,
26+
publishableKey,
27+
});
28+
29+
const clerkUIPreload = `
30+
<link rel="preload"
31+
href="${clerkUIScriptSrc}"
32+
as="script"
33+
crossOrigin="anonymous"
34+
/>`;
35+
36+
return clerkUIPreload + '\n';
37+
}
38+
1539
const clerkJsScriptSrc = clerkJSScriptUrl({
1640
clerkJSUrl: env.clerkJsUrl,
1741
clerkJSVersion: env.clerkJsVersion,

packages/astro/src/server/get-safe-env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ function getSafeEnv(context: ContextOrLocals) {
3535
clerkUIUrl: getContextEnvVar('PUBLIC_CLERK_UI_URL', context),
3636
clerkUIVersion: getContextEnvVar('PUBLIC_CLERK_UI_VERSION', context),
3737
prefetchUI: getContextEnvVar('PUBLIC_CLERK_PREFETCH_UI', context) === 'false' ? false : undefined,
38+
skipJsCdn: getContextEnvVar('PUBLIC_CLERK_SKIP_JS_CDN', context) === 'true',
3839
apiVersion: getContextEnvVar('CLERK_API_VERSION', context),
3940
apiUrl: getContextEnvVar('CLERK_API_URL', context),
4041
telemetryDisabled: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DISABLED', context)),

packages/clerk-js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"default": "./dist/esm/entry.mjs"
6363
},
6464
"./internal": {
65-
"types": "./dist/esm/internal/index.d.mts",
65+
"types": "./dist/types/internal/index.d.ts",
6666
"import": "./dist/esm/internal/index.mjs",
6767
"default": "./dist/esm/internal/index.mjs"
6868
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`module exports > bundled export (bundled.ts) > should have the expected shape 1`] = `
4+
[
5+
"ClerkJS",
6+
"__brand",
7+
"version",
8+
]
9+
`;
10+
11+
exports[`module exports > server export (server.ts) > should have the expected shape 1`] = `
12+
[
13+
"__brand",
14+
"version",
15+
]
16+
`;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { js } from '../bundled';
4+
import { js as serverJs } from '../server';
5+
6+
describe('module exports', () => {
7+
describe('bundled export (bundled.ts)', () => {
8+
it('should have the expected shape', () => {
9+
expect(Object.keys(js).sort()).toMatchSnapshot();
10+
});
11+
12+
it('should include __brand marker', () => {
13+
expect((js as any).__brand).toBe('__clerkJS');
14+
});
15+
16+
it('should include ClerkJS constructor', () => {
17+
expect((js as any).ClerkJS).toBeDefined();
18+
expect(typeof (js as any).ClerkJS).toBe('function');
19+
});
20+
21+
it('should include version', () => {
22+
expect((js as any).version).toBeDefined();
23+
expect(typeof (js as any).version).toBe('string');
24+
});
25+
});
26+
27+
describe('server export (server.ts)', () => {
28+
it('should have the expected shape', () => {
29+
expect(Object.keys(serverJs).sort()).toMatchSnapshot();
30+
});
31+
32+
it('should include __brand marker', () => {
33+
expect((serverJs as any).__brand).toBe('__clerkJS');
34+
});
35+
36+
it('should NOT include ClerkJS constructor', () => {
37+
expect((serverJs as any).ClerkJS).toBeUndefined();
38+
});
39+
40+
it('should include version', () => {
41+
expect((serverJs as any).version).toBeDefined();
42+
expect(typeof (serverJs as any).version).toBe('string');
43+
});
44+
});
45+
});

packages/clerk-js/src/entry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
* Entry point for dynamic import of ClerkJS constructor.
33
* Used by the SDK when the js prop is a server-safe marker (without ClerkJS constructor).
44
*/
5-
export { Clerk as ClerkJs } from './core/clerk';
5+
export { Clerk as ClerkJS } from './core/clerk';

0 commit comments

Comments
 (0)