diff --git a/.changeset/proud-horses-smell.md b/.changeset/proud-horses-smell.md
new file mode 100644
index 00000000000..974bfd4599b
--- /dev/null
+++ b/.changeset/proud-horses-smell.md
@@ -0,0 +1,5 @@
+---
+"@clerk/astro": minor
+---
+
+Add support for Astro `static` and `hybrid` outputs.
diff --git a/integration/presets/astro.ts b/integration/presets/astro.ts
index a34a07e4776..5b1bb70ec84 100644
--- a/integration/presets/astro.ts
+++ b/integration/presets/astro.ts
@@ -20,6 +20,9 @@ const astroNode = applicationConfig()
.addDependency('@clerk/types', clerkTypesLocal)
.addDependency('@clerk/localizations', clerkLocalizationLocal);
+const astroStatic = astroNode.clone().setName('astro-hybrid').useTemplate(templates['astro-hybrid']);
+
export const astro = {
node: astroNode,
-};
+ static: astroStatic,
+} as const;
diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts
index 77f8a27b428..4b08e8e6317 100644
--- a/integration/presets/longRunningApps.ts
+++ b/integration/presets/longRunningApps.ts
@@ -25,6 +25,7 @@ export const createLongRunningApps = () => {
{ id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart },
{ id: 'elements.next.appRouter', config: elements.nextAppRouter, env: envs.withEmailCodes },
{ id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles },
+ { id: 'astro.static.withCustomRoles', config: astro.static, env: envs.withCustomRoles },
{ id: 'expo.expo-web', config: expo.expoWeb, env: envs.withEmailCodes },
] as const;
diff --git a/integration/templates/astro-hybrid/.gitignore b/integration/templates/astro-hybrid/.gitignore
new file mode 100644
index 00000000000..016b59ea143
--- /dev/null
+++ b/integration/templates/astro-hybrid/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/integration/templates/astro-hybrid/astro.config.mjs b/integration/templates/astro-hybrid/astro.config.mjs
new file mode 100644
index 00000000000..30ff739e8a3
--- /dev/null
+++ b/integration/templates/astro-hybrid/astro.config.mjs
@@ -0,0 +1,11 @@
+import { defineConfig } from 'astro/config';
+import clerk from '@clerk/astro';
+import react from '@astrojs/react';
+
+export default defineConfig({
+ output: 'hybrid',
+ integrations: [clerk(), react()],
+ server: {
+ port: Number(process.env.PORT),
+ },
+});
diff --git a/integration/templates/astro-hybrid/package.json b/integration/templates/astro-hybrid/package.json
new file mode 100644
index 00000000000..e5600f80bb0
--- /dev/null
+++ b/integration/templates/astro-hybrid/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "astro-clerk-hybrid-playground",
+ "type": "module",
+ "version": "0.0.1",
+ "scripts": {
+ "dev": "astro dev",
+ "start": "astro dev --port $PORT",
+ "build": "astro check && astro build",
+ "preview": "astro preview --port $PORT",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/check": "^0.7.0",
+ "@astrojs/react": "^3.6.0",
+ "@astrojs/node": "^8.3.2",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "astro": "^4.11.5",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "typescript": "^5.5.3"
+ }
+}
diff --git a/integration/templates/astro-hybrid/public/favicon.svg b/integration/templates/astro-hybrid/public/favicon.svg
new file mode 100644
index 00000000000..f157bd1c5e2
--- /dev/null
+++ b/integration/templates/astro-hybrid/public/favicon.svg
@@ -0,0 +1,9 @@
+
diff --git a/integration/templates/astro-hybrid/src/layouts/Layout.astro b/integration/templates/astro-hybrid/src/layouts/Layout.astro
new file mode 100644
index 00000000000..b5c6c9717ba
--- /dev/null
+++ b/integration/templates/astro-hybrid/src/layouts/Layout.astro
@@ -0,0 +1,24 @@
+---
+interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+---
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
diff --git a/integration/templates/astro-hybrid/src/middleware.ts b/integration/templates/astro-hybrid/src/middleware.ts
new file mode 100644
index 00000000000..cd6f0baf9d5
--- /dev/null
+++ b/integration/templates/astro-hybrid/src/middleware.ts
@@ -0,0 +1,3 @@
+import { clerkMiddleware } from '@clerk/astro/server';
+
+export const onRequest = clerkMiddleware();
diff --git a/integration/templates/astro-hybrid/src/pages/index.astro b/integration/templates/astro-hybrid/src/pages/index.astro
new file mode 100644
index 00000000000..b13e67a0a46
--- /dev/null
+++ b/integration/templates/astro-hybrid/src/pages/index.astro
@@ -0,0 +1,19 @@
+---
+import { UserButton, SignInButton, SignedIn, SignedOut } from "@clerk/astro/components";
+import { OrganizationSwitcher } from "@clerk/astro/react";
+import Layout from "../layouts/Layout.astro";
+
+export const prerender = true;
+---
+
+
+
+ Signed out
+
+
+
+ Signed in
+
+
+
+
diff --git a/integration/templates/astro-hybrid/src/pages/only-admins.astro b/integration/templates/astro-hybrid/src/pages/only-admins.astro
new file mode 100644
index 00000000000..14465c76c80
--- /dev/null
+++ b/integration/templates/astro-hybrid/src/pages/only-admins.astro
@@ -0,0 +1,13 @@
+---
+import { Protect } from "@clerk/astro/components";
+import Layout from "../layouts/Layout.astro";
+
+export const prerender = true;
+---
+
+
+
+ I'm an admin
+ Not an admin
+
+
diff --git a/integration/templates/astro-hybrid/src/pages/only-members.astro b/integration/templates/astro-hybrid/src/pages/only-members.astro
new file mode 100644
index 00000000000..70eac5ff274
--- /dev/null
+++ b/integration/templates/astro-hybrid/src/pages/only-members.astro
@@ -0,0 +1,13 @@
+---
+import { Protect } from "@clerk/astro/components";
+import Layout from "../layouts/Layout.astro";
+
+export const prerender = false;
+---
+
+
+
+ I'm a member
+ Not a member
+
+
diff --git a/integration/templates/astro-hybrid/src/pages/ssr.astro b/integration/templates/astro-hybrid/src/pages/ssr.astro
new file mode 100644
index 00000000000..b0ad0b253ef
--- /dev/null
+++ b/integration/templates/astro-hybrid/src/pages/ssr.astro
@@ -0,0 +1,19 @@
+---
+import { UserButton, SignInButton, SignedIn, SignedOut } from "@clerk/astro/components";
+import { OrganizationSwitcher } from "@clerk/astro/react";
+import Layout from "../layouts/Layout.astro";
+
+export const prerender = false;
+---
+
+
+
+ Signed out
+
+
+
+ Signed in
+
+
+
+
diff --git a/integration/templates/astro-hybrid/tsconfig.json b/integration/templates/astro-hybrid/tsconfig.json
new file mode 100644
index 00000000000..b7243b92ccf
--- /dev/null
+++ b/integration/templates/astro-hybrid/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react"
+ }
+}
diff --git a/integration/templates/index.ts b/integration/templates/index.ts
index e4c2f4ad2fe..791590c666f 100644
--- a/integration/templates/index.ts
+++ b/integration/templates/index.ts
@@ -12,6 +12,7 @@ export const templates = {
'remix-node': resolve(__dirname, './remix-node'),
'elements-next': resolve(__dirname, './elements-next'),
'astro-node': resolve(__dirname, './astro-node'),
+ 'astro-hybrid': resolve(__dirname, './astro-hybrid'),
'expo-web': resolve(__dirname, './expo-web'),
} as const;
diff --git a/integration/tests/astro/hybrid.test.ts b/integration/tests/astro/hybrid.test.ts
new file mode 100644
index 00000000000..a0ff4c92fb3
--- /dev/null
+++ b/integration/tests/astro/hybrid.test.ts
@@ -0,0 +1,114 @@
+import { expect, test } from '@playwright/test';
+
+import type { FakeOrganization, FakeUser } from '../../testUtils';
+import { createTestUtils, testAgainstRunningApps } from '../../testUtils';
+
+testAgainstRunningApps({ withPattern: ['astro.static.withCustomRoles'] })(
+ 'basic flows for @astro hybrid output',
+ ({ app }) => {
+ test.describe.configure({ mode: 'serial' });
+
+ let fakeAdmin: FakeUser;
+ let fakeOrganization: FakeOrganization;
+ let fakeAdmin2: FakeUser;
+ let fakeOrganization2: FakeOrganization;
+
+ test.beforeAll(async () => {
+ const m = createTestUtils({ app });
+ fakeAdmin = m.services.users.createFakeUser();
+ const admin = await m.services.users.createBapiUser(fakeAdmin);
+ fakeOrganization = await m.services.users.createFakeOrganization(admin.id);
+
+ fakeAdmin2 = m.services.users.createFakeUser();
+ const admin2 = await m.services.users.createBapiUser(fakeAdmin2);
+ fakeOrganization2 = await m.services.users.createFakeOrganization(admin2.id);
+ });
+
+ test.afterAll(async () => {
+ await fakeOrganization.delete();
+ await fakeAdmin.deleteIfExists();
+
+ await fakeOrganization2.delete();
+ await fakeAdmin2.deleteIfExists();
+ await app.teardown();
+ });
+
+ test('render SignedIn and SignedOut contents (prerendered)', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToAppHome();
+
+ await u.page.waitForClerkJsLoaded();
+
+ await u.po.expect.toBeSignedOut();
+ await expect(u.page.getByText('Signed out')).toBeVisible();
+ await expect(u.page.getByText('Signed in')).toBeHidden();
+
+ await u.page.getByRole('button', { name: /Sign in/i }).click();
+
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
+
+ await expect(u.page.getByText('Signed out')).toBeHidden();
+ await expect(u.page.getByText('Signed in')).toBeVisible();
+ });
+
+ test('render SignedIn and SignedOut contents (SSR)', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/ssr');
+
+ await u.page.waitForClerkJsLoaded();
+
+ await u.po.expect.toBeSignedOut();
+ await expect(u.page.getByText('Signed out')).toBeVisible();
+ await expect(u.page.getByText('Signed in')).toBeHidden();
+
+ await u.page.getByRole('button', { name: /Sign in/i }).click();
+
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
+
+ await expect(u.page.getByText('Signed out')).toBeHidden();
+ await expect(u.page.getByText('Signed in')).toBeVisible();
+ });
+
+ test('render Protect contents for admin', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToAppHome();
+
+ await u.page.waitForClerkJsLoaded();
+
+ // Sign in
+ await u.page.getByRole('button', { name: /Sign in/i }).click();
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
+
+ // Select an organization
+ await u.po.organizationSwitcher.waitForMounted();
+ await u.po.organizationSwitcher.waitForAnOrganizationToSelected();
+
+ await u.page.goToRelative('/only-admins');
+
+ await expect(u.page.getByText("I'm an admin")).toBeVisible();
+ });
+
+ test('render Protect fallback', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToAppHome();
+
+ await u.page.waitForClerkJsLoaded();
+
+ // Sign in
+ await u.page.getByRole('button', { name: /Sign in/i }).click();
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
+
+ await u.page.goToRelative('/only-members');
+
+ await expect(u.page.getByText('Not a member')).toBeVisible();
+ });
+ },
+);
diff --git a/packages/astro/.eslintrc.cjs b/packages/astro/.eslintrc.cjs
index 39dd0364e5a..84bc665e0e0 100644
--- a/packages/astro/.eslintrc.cjs
+++ b/packages/astro/.eslintrc.cjs
@@ -4,11 +4,7 @@ module.exports = {
rules: {
'import/no-unresolved': ['error', { ignore: ['^#'] }],
},
- ignorePatterns: [
- 'src/astro-components/index.ts',
- 'src/astro-components/interactive/UserButton/index.ts',
- 'src/astro-components/interactive/UserProfile/index.ts',
- ],
+ ignorePatterns: ['src/astro-components/**/*.ts'],
overrides: [
{
files: ['./env.d.ts'],
diff --git a/packages/astro/.gitignore b/packages/astro/.gitignore
index 29d852eaa1c..17fa296850d 100644
--- a/packages/astro/.gitignore
+++ b/packages/astro/.gitignore
@@ -5,6 +5,7 @@ astro-components/
components/
!src/components/
.output/
+/types.ts
.vscode/
.idea/
diff --git a/packages/astro/bundled/package.json b/packages/astro/bundled/package.json
deleted file mode 100644
index a59118805b6..00000000000
--- a/packages/astro/bundled/package.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "main": "../dist/bundled.js"
-}
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 5ed154c0c29..41009fd79dc 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -30,7 +30,7 @@
"dev": "tsup --watch --onSuccess \"npm run build:dts\"",
"build": "tsup --onSuccess \"npm run build:dts\" && npm run copy:components",
"build:dts": "tsc --emitDeclarationOnly --declaration",
- "copy:components": "rm -rf ./components && mkdir -p ./components/ && cp -r ./src/astro-components/* ./components/",
+ "copy:components": "rm -rf ./components && mkdir -p ./components/ && cp -r ./src/astro-components/* ./components/ && cp ./src/types.ts ./",
"lint": "eslint src/",
"lint:attw": "attw --pack .",
"lint:publint": "publint",
@@ -38,12 +38,12 @@
},
"files": [
"dist",
- "bundled",
"client",
"server",
"internal",
"components",
- "env.d.ts"
+ "env.d.ts",
+ "types.ts"
],
"exports": {
".": {
diff --git a/packages/astro/src/astro-components/control/BaseClerkControlElement.ts b/packages/astro/src/astro-components/control/BaseClerkControlElement.ts
new file mode 100644
index 00000000000..f992af25978
--- /dev/null
+++ b/packages/astro/src/astro-components/control/BaseClerkControlElement.ts
@@ -0,0 +1,35 @@
+import { $authStore, $isLoadedStore } from '@clerk/astro/client';
+
+export type AuthState = ReturnType;
+
+export class BaseClerkControlElement extends HTMLElement {
+ protected authStoreListener: (() => void) | null = null;
+ protected isLoadedStoreListener: (() => void) | null = null;
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ this.isLoadedStoreListener = $isLoadedStore.subscribe(loaded => {
+ if (loaded) {
+ this.toggleContentVisibility();
+ }
+ });
+ }
+
+ disconnectedCallback() {
+ this.authStoreListener?.();
+ this.isLoadedStoreListener?.();
+ }
+
+ toggleContentVisibility() {
+ this.authStoreListener = $authStore.subscribe(state => {
+ this.onAuthStateChange(state);
+ });
+ }
+
+ // This method will be overridden by subclasses
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ protected onAuthStateChange(state: AuthState): void {}
+}
diff --git a/packages/astro/src/astro-components/control/Protect.astro b/packages/astro/src/astro-components/control/Protect.astro
index 74ba18846d1..e1ccd138c31 100644
--- a/packages/astro/src/astro-components/control/Protect.astro
+++ b/packages/astro/src/astro-components/control/Protect.astro
@@ -1,38 +1,35 @@
---
-import type {
- CheckAuthorizationWithCustomPermissions,
- OrganizationCustomPermissionKey,
- OrganizationCustomRoleKey,
-} from '@clerk/types';
+import ProtectCSR from './ProtectCSR.astro';
+import ProtectSSR from './ProtectSSR.astro';
-type Props =
- | {
- condition?: never;
- role: OrganizationCustomRoleKey;
- permission?: never;
- }
- | {
- condition?: never;
- role?: never;
- permission: OrganizationCustomPermissionKey;
- }
- | {
- condition: (has: CheckAuthorizationWithCustomPermissions) => boolean;
- role?: never;
- permission?: never;
- }
- | {
- condition?: never;
- role?: never;
- permission?: never;
- };
+import { isStaticOutput } from "virtual:@clerk/astro/config";
+import type { ProtectProps } from '../../types';
-const { has, userId } = Astro.locals.auth();
-const isUnauthorized =
- !userId ||
- (typeof Astro.props.condition === "function" &&
- !Astro.props.condition(has)) ||
- ((Astro.props.role || Astro.props.permission) && !has(Astro.props));
+type Props = ProtectProps & {
+ isStatic?: boolean
+ /**
+ * The class name to apply to the outermost element of the component.
+ * This class is only applied to static components.
+ */
+ class?: string;
+ /**
+ * The class name to apply to the wrapper element of the default slot.
+ * This class is only applied to static components.
+ */
+ defaultSlotWrapperClass?: string;
+ /**
+ * The class name to apply to the wrapper element of the fallback slot.
+ * This class is only applied to static components.
+ */
+ fallbackSlotWrapperClass?: string;
+}
+
+const { isStatic, ...props } = Astro.props;
+
+const ProtectComponent = isStaticOutput(isStatic) ? ProtectCSR : ProtectSSR;
---
-{isUnauthorized ? : }
+
+
+
+
diff --git a/packages/astro/src/astro-components/control/ProtectCSR.astro b/packages/astro/src/astro-components/control/ProtectCSR.astro
new file mode 100644
index 00000000000..18a02b18128
--- /dev/null
+++ b/packages/astro/src/astro-components/control/ProtectCSR.astro
@@ -0,0 +1,57 @@
+---
+import type { ProtectProps } from '../../types';
+
+type Props = Omit & {
+ class?: string;
+ defaultSlotWrapperClass?: string;
+ fallbackSlotWrapperClass?: string;
+};
+
+const { role, permission, class: className, defaultSlotWrapperClass, fallbackSlotWrapperClass } = Astro.props;
+---
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/astro/src/astro-components/control/ProtectSSR.astro b/packages/astro/src/astro-components/control/ProtectSSR.astro
new file mode 100644
index 00000000000..4f0d58a98ea
--- /dev/null
+++ b/packages/astro/src/astro-components/control/ProtectSSR.astro
@@ -0,0 +1,14 @@
+---
+import type { ProtectProps } from '../../types';
+
+type Props = ProtectProps;
+
+const { has, userId } = Astro.locals.auth();
+const isUnauthorized =
+ !userId ||
+ (typeof Astro.props.condition === "function" &&
+ !Astro.props.condition(has)) ||
+ ((Astro.props.role || Astro.props.permission) && !has(Astro.props));
+---
+
+{isUnauthorized ? : }
diff --git a/packages/astro/src/astro-components/control/SignedIn.astro b/packages/astro/src/astro-components/control/SignedIn.astro
index c1657391909..4c6c33b0a4c 100644
--- a/packages/astro/src/astro-components/control/SignedIn.astro
+++ b/packages/astro/src/astro-components/control/SignedIn.astro
@@ -1,6 +1,23 @@
---
-const { userId } = Astro.locals.auth()
----
+import SignedInCSR from './SignedInCSR.astro';
+import SignedInSSR from './SignedInSSR.astro';
+
+import { isStaticOutput } from 'virtual:@clerk/astro/config';
+
+type Props = {
+ isStatic?: boolean
+ /**
+ * The class name to apply to the outermost element of the component.
+ * This class is only applied to static components.
+ */
+ class?: string
+}
+const { isStatic, class: className } = Astro.props
+
+const SignedInComponent = isStaticOutput(isStatic) ? SignedInCSR : SignedInSSR;
+---
-{ userId ? : null }
\ No newline at end of file
+
+
+
diff --git a/packages/astro/src/astro-components/control/SignedInCSR.astro b/packages/astro/src/astro-components/control/SignedInCSR.astro
new file mode 100644
index 00000000000..c1010a4a179
--- /dev/null
+++ b/packages/astro/src/astro-components/control/SignedInCSR.astro
@@ -0,0 +1,27 @@
+---
+type Props = {
+ class?: string
+}
+
+const { class: className } = Astro.props
+---
+
+
+
+
+
+
diff --git a/packages/astro/src/astro-components/control/SignedInSSR.astro b/packages/astro/src/astro-components/control/SignedInSSR.astro
new file mode 100644
index 00000000000..4253bcfe875
--- /dev/null
+++ b/packages/astro/src/astro-components/control/SignedInSSR.astro
@@ -0,0 +1,5 @@
+---
+const { userId } = Astro.locals.auth()
+---
+
+{ userId ? : null }
diff --git a/packages/astro/src/astro-components/control/SignedOut.astro b/packages/astro/src/astro-components/control/SignedOut.astro
index bbcf2c8cc5c..bbc0be0997c 100644
--- a/packages/astro/src/astro-components/control/SignedOut.astro
+++ b/packages/astro/src/astro-components/control/SignedOut.astro
@@ -1,6 +1,23 @@
---
-const { userId } = Astro.locals.auth()
----
+import SignedOutCSR from './SignedOutCSR.astro';
+import SignedOutSSR from './SignedOutSSR.astro';
+
+import { isStaticOutput } from 'virtual:@clerk/astro/config';
+
+type Props = {
+ isStatic?: boolean
+ /**
+ * The class name to apply to the outermost element of the component.
+ * This class is only applied to static components.
+ */
+ class?: string
+}
+const { isStatic, class: className } = Astro.props
+
+const SignedOutComponent = isStaticOutput(isStatic) ? SignedOutCSR : SignedOutSSR;
+---
-{ !userId ? : null }
\ No newline at end of file
+
+
+c
diff --git a/packages/astro/src/astro-components/control/SignedOutCSR.astro b/packages/astro/src/astro-components/control/SignedOutCSR.astro
new file mode 100644
index 00000000000..4540ef42ec9
--- /dev/null
+++ b/packages/astro/src/astro-components/control/SignedOutCSR.astro
@@ -0,0 +1,27 @@
+---
+type Props = {
+ class?: string
+}
+
+const { class: className } = Astro.props
+---
+
+
+
+
+
+
diff --git a/packages/astro/src/astro-components/control/SignedOutSSR.astro b/packages/astro/src/astro-components/control/SignedOutSSR.astro
new file mode 100644
index 00000000000..8b1b8154ad9
--- /dev/null
+++ b/packages/astro/src/astro-components/control/SignedOutSSR.astro
@@ -0,0 +1,5 @@
+---
+const { userId } = Astro.locals.auth()
+---
+
+{ !userId ? : null }
diff --git a/packages/astro/src/env.d.ts b/packages/astro/src/env.d.ts
index 57dc7096ab1..0d0c534341c 100644
--- a/packages/astro/src/env.d.ts
+++ b/packages/astro/src/env.d.ts
@@ -29,3 +29,10 @@ declare namespace App {
runtime: { env: InternalEnv };
}
}
+
+declare module 'virtual:@clerk/astro/config' {
+ import type { AstroConfig } from 'astro';
+
+ export const astroConfig: AstroConfig;
+ export function isStaticOutput(forceStatic?: boolean): boolean;
+}
diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts
index 246bba537af..b640dedbbf4 100644
--- a/packages/astro/src/integration/create-integration.ts
+++ b/packages/astro/src/integration/create-integration.ts
@@ -3,6 +3,7 @@ import type { AstroIntegration } from 'astro';
import { name as packageName, version as packageVersion } from '../../package.json';
import type { AstroClerkIntegrationParams } from '../types';
+import { vitePluginAstroConfig } from './vite-plugin-astro-config';
const buildEnvVarFromOption = (valueToBeStored: unknown, envName: keyof InternalEnv) => {
return valueToBeStored ? { [`import.meta.env.${envName}`]: JSON.stringify(valueToBeStored) } : {};
@@ -27,11 +28,7 @@ function createIntegration()
name: '@clerk/astro/integration',
hooks: {
'astro:config:setup': ({ config, injectScript, updateConfig, logger, command }) => {
- if (config.output === 'static') {
- logger.error(`${packageName} requires SSR to be turned on. Please update output to "server"`);
- }
-
- if (!config.adapter) {
+ if (['server', 'hybrid'].includes(config.output) && !config.adapter) {
logger.error('Missing adapter, please update your Astro config to use one.');
}
@@ -53,6 +50,7 @@ function createIntegration()
// Set params as envs so backend code has access to them
updateConfig({
vite: {
+ plugins: [vitePluginAstroConfig(config)],
define: {
/**
* Convert the integration params to environment variable in order for it to be readable from the server
@@ -62,8 +60,6 @@ function createIntegration()
...buildEnvVarFromOption(isSatellite, 'PUBLIC_CLERK_IS_SATELLITE'),
...buildEnvVarFromOption(proxyUrl, 'PUBLIC_CLERK_PROXY_URL'),
...buildEnvVarFromOption(domain, 'PUBLIC_CLERK_DOMAIN'),
- ...buildEnvVarFromOption(domain, 'PUBLIC_CLERK_DOMAIN'),
- ...buildEnvVarFromOption(domain, 'PUBLIC_CLERK_DOMAIN'),
...buildEnvVarFromOption(clerkJSUrl, 'PUBLIC_CLERK_JS_URL'),
...buildEnvVarFromOption(clerkJSVariant, 'PUBLIC_CLERK_JS_VARIANT'),
...buildEnvVarFromOption(clerkJSVersion, 'PUBLIC_CLERK_JS_VERSION'),
diff --git a/packages/astro/src/integration/vite-plugin-astro-config.ts b/packages/astro/src/integration/vite-plugin-astro-config.ts
new file mode 100644
index 00000000000..f9505aeb580
--- /dev/null
+++ b/packages/astro/src/integration/vite-plugin-astro-config.ts
@@ -0,0 +1,44 @@
+import type { AstroConfig } from 'astro';
+
+type VitePlugin = Required['plugins'][number];
+
+/**
+ * This Vite module exports a `isStaticOutput` function that is imported inside our control components
+ * to determine which components to use depending on the Astro config output option.
+ *
+ * @param {AstroConfig} astroConfig - The Astro configuration object
+ * @returns {VitePlugin} A Vite plugin
+ */
+export function vitePluginAstroConfig(astroConfig: AstroConfig): VitePlugin {
+ const virtualModuleId = 'virtual:@clerk/astro/config';
+ const resolvedVirtualModuleId = '\0' + virtualModuleId;
+
+ return {
+ name: 'vite-plugin-astro-config',
+ resolveId(id) {
+ if (id === virtualModuleId) {
+ return resolvedVirtualModuleId;
+ }
+ },
+ load(id) {
+ if (id === resolvedVirtualModuleId) {
+ return `
+ export const astroConfig = ${JSON.stringify(astroConfig)};
+
+ export function isStaticOutput(forceStatic) {
+ if (astroConfig.output === 'hybrid' && forceStatic === undefined) {
+ // Default page is prerendered in hybrid mode
+ return true;
+ }
+
+ if (forceStatic !== undefined) {
+ return forceStatic;
+ }
+
+ return astroConfig.output === 'static';
+ }
+ `;
+ }
+ },
+ };
+}
diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx
index 22c1bd65ccf..5f04461401e 100644
--- a/packages/astro/src/react/controlComponents.tsx
+++ b/packages/astro/src/react/controlComponents.tsx
@@ -1,14 +1,10 @@
-import type {
- CheckAuthorizationWithCustomPermissions,
- HandleOAuthCallbackParams,
- OrganizationCustomPermissionKey,
- OrganizationCustomRoleKey,
-} from '@clerk/types';
+import type { CheckAuthorizationWithCustomPermissions, HandleOAuthCallbackParams } from '@clerk/types';
import { computed } from 'nanostores';
import type { PropsWithChildren } from 'react';
import React, { useEffect, useState } from 'react';
import { $csrState } from '../stores/internal';
+import type { ProtectProps as _ProtectProps } from '../types';
import { useAuth } from './hooks';
import type { WithClerkProp } from './utils';
import { withClerk } from './utils';
@@ -73,32 +69,7 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JSX.Element
return <>{children}>;
};
-export type ProtectProps = React.PropsWithChildren<
- (
- | {
- condition?: never;
- role: OrganizationCustomRoleKey;
- permission?: never;
- }
- | {
- condition?: never;
- role?: never;
- permission: OrganizationCustomPermissionKey;
- }
- | {
- condition: (has: CheckAuthorizationWithCustomPermissions) => boolean;
- role?: never;
- permission?: never;
- }
- | {
- condition?: never;
- role?: never;
- permission?: never;
- }
- ) & {
- fallback?: React.ReactNode;
- }
->;
+export type ProtectProps = React.PropsWithChildren<_ProtectProps & { fallback?: React.ReactNode }>;
/**
* Use `` in order to prevent unauthenticated or unauthorized users from accessing the children passed to the component.
diff --git a/packages/astro/src/stores/external.ts b/packages/astro/src/stores/external.ts
index 3ce77d20a8f..5dbcb2ef88d 100644
--- a/packages/astro/src/stores/external.ts
+++ b/packages/astro/src/stores/external.ts
@@ -4,6 +4,16 @@ import { batched, computed, onMount, type Store } from 'nanostores';
import { $clerk, $csrState, $initialState } from './internal';
+/**
+ * A client side store that returns the loaded state of clerk-js.
+ *
+ * @example
+ * A simple example:
+ *
+ * $isLoadedStore.subscribe((authloaded => console.log(loaded))
+ */
+export const $isLoadedStore = computed([$csrState], state => state.isLoaded);
+
/**
* A client side store that is prepopulated with the authentication context during SSR.
* It is a nanostore, for instructions on how to use nanostores please review the [documentation](https://github.com/nanostores/nanostores)
diff --git a/packages/astro/src/stores/utils.ts b/packages/astro/src/stores/utils.ts
deleted file mode 100644
index efb6aadef9f..00000000000
--- a/packages/astro/src/stores/utils.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import type {
- ActiveSessionResource,
- InitialState,
- OrganizationCustomPermissionKey,
- OrganizationCustomRoleKey,
- OrganizationResource,
- Resources,
- UserResource,
-} from '@clerk/types';
-
-export function deriveState(clerkLoaded: boolean, state: Resources, initialState: InitialState | undefined) {
- if (!clerkLoaded && initialState) {
- return {
- ...deriveFromSsrInitialState(initialState),
- clerkLoaded,
- };
- }
-
- return {
- ...deriveFromClientSideState(state),
- clerkLoaded,
- };
-}
-
-function deriveFromSsrInitialState(initialState: InitialState) {
- const userId = initialState.userId;
- const user = initialState.user as any as UserResource;
- const sessionId = initialState.sessionId;
- const session = initialState.session as any as ActiveSessionResource;
- const organization = initialState.organization as any 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,
- };
-}
-
-function 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/astro/src/types.ts b/packages/astro/src/types.ts
index 9462b751ec2..f70631f5eb5 100644
--- a/packages/astro/src/types.ts
+++ b/packages/astro/src/types.ts
@@ -1,4 +1,13 @@
-import type { Clerk, ClerkOptions, ClientResource, MultiDomainAndOrProxyPrimitives, Without } from '@clerk/types';
+import type {
+ CheckAuthorizationWithCustomPermissions,
+ Clerk,
+ ClerkOptions,
+ ClientResource,
+ MultiDomainAndOrProxyPrimitives,
+ OrganizationCustomPermissionKey,
+ OrganizationCustomRoleKey,
+ Without,
+} from '@clerk/types';
type AstroClerkUpdateOptions = Pick;
@@ -38,4 +47,26 @@ declare global {
}
}
-export type { AstroClerkUpdateOptions, AstroClerkIntegrationParams, AstroClerkCreateInstanceParams };
+type ProtectProps =
+ | {
+ condition?: never;
+ role: OrganizationCustomRoleKey;
+ permission?: never;
+ }
+ | {
+ condition?: never;
+ role?: never;
+ permission: OrganizationCustomPermissionKey;
+ }
+ | {
+ condition: (has: CheckAuthorizationWithCustomPermissions) => boolean;
+ role?: never;
+ permission?: never;
+ }
+ | {
+ condition?: never;
+ role?: never;
+ permission?: never;
+ };
+
+export type { AstroClerkUpdateOptions, AstroClerkIntegrationParams, AstroClerkCreateInstanceParams, ProtectProps };
diff --git a/packages/astro/tsconfig.json b/packages/astro/tsconfig.json
index d1e9ae67c05..514256274a8 100644
--- a/packages/astro/tsconfig.json
+++ b/packages/astro/tsconfig.json
@@ -24,5 +24,12 @@
"declarationDir": "dist/types",
"noUncheckedIndexedAccess": true
},
- "exclude": ["dist", "build", "node_modules", "src/astro-components"]
+ "exclude": [
+ "dist",
+ "build",
+ "node_modules",
+ // We're ignoring the files below because they are published as-is and is not processed/bundled with tsup.
+ // This cause TS to throw errors even though we're not bundling it.
+ "src/astro-components/**/*.ts"
+ ]
}