From 9e716517ad920f5ba7737ceebcda7d7316abee29 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 5 Sep 2024 17:23:49 -0500 Subject: [PATCH 01/37] update clerk/ui for rendering --- packages/ui/package.json | 10 ++++++---- packages/ui/src/common/phone-number-field.tsx | 2 +- packages/ui/src/components/sign-in/sign-in.tsx | 5 +++-- .../ui/src/components/sign-in/steps/choose-session.tsx | 2 +- .../src/components/sign-in/steps/choose-strategy.tsx | 8 ++------ packages/ui/src/components/sign-up/steps/start.tsx | 7 +++++-- .../ui/src/components/sign-up/steps/verifications.tsx | 2 +- packages/ui/src/hooks/use-display-config.ts | 2 +- packages/ui/src/hooks/use-enabled-connections.ts | 2 +- packages/ui/src/hooks/use-environment.ts | 2 +- packages/ui/src/hooks/use-options.ts | 9 ++------- packages/ui/src/hooks/use-reset-password-factor.ts | 2 +- packages/ui/src/primitives/animated.tsx | 2 -- packages/ui/tsup.config.ts | 2 +- 14 files changed, 26 insertions(+), 31 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index e93cb0a4789..f1df98613f7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -27,13 +27,16 @@ "./*": { "import": { "types": "./dist/components/*.d.mts", - "default": "./dist/components/*.mjs" + "default": "./dist/components/*.mjs", + "browser": "./dist/components/*.mjs" }, "require": { "types": "./dist/components/*.d.ts", - "default": "./dist/components/*.js" + "default": "./dist/components/*.js", + "browser": "./dist/components/*.js" } - } + }, + "./styles.css": "./dist/styles.css" }, "main": "index.js", "files": [ @@ -47,7 +50,6 @@ "test": "vitest" }, "dependencies": { - "@clerk/clerk-react": "file:../react", "@clerk/elements": "file:../elements", "@clerk/shared": "file:../shared", "@clerk/types": "file:../types", diff --git a/packages/ui/src/common/phone-number-field.tsx b/packages/ui/src/common/phone-number-field.tsx index 9ed2fa5f859..438d2c0c92f 100644 --- a/packages/ui/src/common/phone-number-field.tsx +++ b/packages/ui/src/common/phone-number-field.tsx @@ -1,5 +1,5 @@ -import { useClerk } from '@clerk/clerk-react'; import * as Common from '@clerk/elements/common'; +import { useClerk } from '@clerk/shared/react'; import { Command } from 'cmdk'; import { cx } from 'cva'; import * as React from 'react'; diff --git a/packages/ui/src/components/sign-in/sign-in.tsx b/packages/ui/src/components/sign-in/sign-in.tsx index 2d24dc89aae..9a5ac9a0714 100644 --- a/packages/ui/src/components/sign-in/sign-in.tsx +++ b/packages/ui/src/components/sign-in/sign-in.tsx @@ -1,4 +1,5 @@ import { Root as SignInRoot } from '@clerk/elements/sign-in'; +import type { SignInProps } from '@clerk/types'; import * as React from 'react'; import { GetHelpContext } from '~/components/sign-in/hooks/use-get-help'; @@ -23,13 +24,13 @@ import { type Appearance, AppearanceProvider } from '~/contexts'; * where we'll consider its integration within Elements, as well as ensure * bulletproof a11y. */ -export function SignIn({ appearance }: { appearance?: Appearance }) { +export function SignIn({ appearance, ...rest }: { appearance?: Appearance } & SignInProps) { const [showHelp, setShowHelp] = React.useState(false); return ( - + {showHelp ? ( ) : ( diff --git a/packages/ui/src/components/sign-in/steps/choose-session.tsx b/packages/ui/src/components/sign-in/steps/choose-session.tsx index 833b82aa25c..c54a750be08 100644 --- a/packages/ui/src/components/sign-in/steps/choose-session.tsx +++ b/packages/ui/src/components/sign-in/steps/choose-session.tsx @@ -1,5 +1,5 @@ -import { useClerk } from '@clerk/clerk-react'; import * as SignIn from '@clerk/elements/sign-in'; +import { useClerk } from '@clerk/shared/react'; import { cva } from 'cva'; import { Button } from 'react-aria-components'; diff --git a/packages/ui/src/components/sign-in/steps/choose-strategy.tsx b/packages/ui/src/components/sign-in/steps/choose-strategy.tsx index ea3b1ed7e3e..518a665400b 100644 --- a/packages/ui/src/components/sign-in/steps/choose-strategy.tsx +++ b/packages/ui/src/components/sign-in/steps/choose-strategy.tsx @@ -1,15 +1,13 @@ -import { useSignIn } from '@clerk/clerk-react'; import * as Common from '@clerk/elements/common'; import * as SignIn from '@clerk/elements/sign-in'; +import { useClerk } from '@clerk/shared/react'; import { Connections } from '~/common/connections'; import { GlobalError } from '~/common/global-error'; import { useGetHelp } from '~/components/sign-in/hooks/use-get-help'; import { LOCALIZATION_NEEDED } from '~/constants/localizations'; -import { useAppearance } from '~/contexts'; import { useCard } from '~/hooks/use-card'; import { useDevModeWarning } from '~/hooks/use-dev-mode-warning'; -import { useEnabledConnections } from '~/hooks/use-enabled-connections'; import { useLocalizations } from '~/hooks/use-localizations'; import { Button } from '~/primitives/button'; import * as Card from '~/primitives/card'; @@ -20,7 +18,7 @@ import { LinkButton } from '~/primitives/link'; ============================================ */ function FirstFactorConnections({ isGlobalLoading }: { isGlobalLoading: boolean }) { - const { signIn } = useSignIn(); + const { signIn } = useClerk().client; const isFirstFactor = signIn?.status === 'needs_first_factor'; if (isFirstFactor) { @@ -33,11 +31,9 @@ function FirstFactorConnections({ isGlobalLoading }: { isGlobalLoading: boolean ============================================ */ export function SignInChooseStrategy() { - const enabledConnections = useEnabledConnections(); const { t } = useLocalizations(); const { setShowHelp } = useGetHelp(); - const hasConnection = enabledConnections.length > 0; const isDev = useDevModeWarning(); const { logoProps, footerProps } = useCard(); diff --git a/packages/ui/src/components/sign-up/steps/start.tsx b/packages/ui/src/components/sign-up/steps/start.tsx index d036c2866f3..bc00a89bfa9 100644 --- a/packages/ui/src/components/sign-up/steps/start.tsx +++ b/packages/ui/src/components/sign-up/steps/start.tsx @@ -1,6 +1,6 @@ -import { useClerk } from '@clerk/clerk-react'; import * as Common from '@clerk/elements/common'; import * as SignUp from '@clerk/elements/sign-up'; +import { useClerk } from '@clerk/shared/react'; import { Connections } from '~/common/connections'; import { EmailField } from '~/common/email-field'; @@ -48,7 +48,10 @@ export function SignUpStart() { {isGlobalLoading => { const connectionsWithSeperator = [ - , + , hasConnection && hasIdentifier ? {t('dividerText')} : null, ]; return ( diff --git a/packages/ui/src/components/sign-up/steps/verifications.tsx b/packages/ui/src/components/sign-up/steps/verifications.tsx index 9498ae1d33e..51e15f06f3c 100644 --- a/packages/ui/src/components/sign-up/steps/verifications.tsx +++ b/packages/ui/src/components/sign-up/steps/verifications.tsx @@ -1,6 +1,6 @@ -import { useClerk } from '@clerk/clerk-react'; import * as Common from '@clerk/elements/common'; import * as SignUp from '@clerk/elements/sign-up'; +import { useClerk } from '@clerk/shared/react'; import { GlobalError } from '~/common/global-error'; import { OTPField } from '~/common/otp-field'; diff --git a/packages/ui/src/hooks/use-display-config.ts b/packages/ui/src/hooks/use-display-config.ts index 7a7a3169eec..e472f282dd1 100644 --- a/packages/ui/src/hooks/use-display-config.ts +++ b/packages/ui/src/hooks/use-display-config.ts @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import type { EnvironmentResource } from '@clerk/types'; export function useDisplayConfig() { diff --git a/packages/ui/src/hooks/use-enabled-connections.ts b/packages/ui/src/hooks/use-enabled-connections.ts index 096839649ed..e019dbe5f8e 100644 --- a/packages/ui/src/hooks/use-enabled-connections.ts +++ b/packages/ui/src/hooks/use-enabled-connections.ts @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import { type EnvironmentResource, OAUTH_PROVIDERS, WEB3_PROVIDERS } from '@clerk/types'; export function useEnabledConnections() { diff --git a/packages/ui/src/hooks/use-environment.ts b/packages/ui/src/hooks/use-environment.ts index c824b3a7032..ee20c7e679d 100644 --- a/packages/ui/src/hooks/use-environment.ts +++ b/packages/ui/src/hooks/use-environment.ts @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import type { EnvironmentResource } from '@clerk/types'; export function useEnvironment() { diff --git a/packages/ui/src/hooks/use-options.ts b/packages/ui/src/hooks/use-options.ts index ec8c06ee295..399f015b709 100644 --- a/packages/ui/src/hooks/use-options.ts +++ b/packages/ui/src/hooks/use-options.ts @@ -1,8 +1,3 @@ -import { useClerk } from '@clerk/clerk-react'; -import type { ClerkOptions } from '@clerk/types'; +import { useOptionsContext } from '@clerk/shared/react'; -export function useOptions() { - const clerk = useClerk(); - const options = (clerk as any)?.options as ClerkOptions; - return options; -} +export const useOptions = useOptionsContext; diff --git a/packages/ui/src/hooks/use-reset-password-factor.ts b/packages/ui/src/hooks/use-reset-password-factor.ts index 502027be2ed..8cb9fd17018 100644 --- a/packages/ui/src/hooks/use-reset-password-factor.ts +++ b/packages/ui/src/hooks/use-reset-password-factor.ts @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import type { ResetPasswordCodeFactor, SignInStrategy } from '@clerk/types'; const resetPasswordStrategies: SignInStrategy[] = ['reset_password_phone_code', 'reset_password_email_code']; diff --git a/packages/ui/src/primitives/animated.tsx b/packages/ui/src/primitives/animated.tsx index b2b3a5c1a32..f4cd85a3504 100644 --- a/packages/ui/src/primitives/animated.tsx +++ b/packages/ui/src/primitives/animated.tsx @@ -7,8 +7,6 @@ type AnimatedProps = PropsWithChildren<{ asChild?: boolean }>; export const Animated = (props: AnimatedProps) => { const { children, asChild } = props; - // TODO: Once https://github.com/clerk/javascript/pull/3976 has been merged read from parsedLayout - // const { animations } = useAppearance().parsedLayout; const { animations } = useAppearance().parsedAppearance.layout; const [parent] = useAutoAnimate(); const ref = animations !== false ? parent : null; diff --git a/packages/ui/tsup.config.ts b/packages/ui/tsup.config.ts index 9d45f791969..aea64896152 100644 --- a/packages/ui/tsup.config.ts +++ b/packages/ui/tsup.config.ts @@ -47,7 +47,7 @@ export default defineConfig(overrideOptions => { 'components/sign-up': 'src/components/sign-up/index.tsx', contexts: 'src/contexts/index.ts', }, - external: ['react', 'react-dom'], + external: ['react', 'react-dom', '@clerk/shared'], format: ['cjs', 'esm'], minify: false, sourcemap: true, From 8f3ea4b546e15bea4e5f7638e4c3ee63022ed306 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 5 Sep 2024 17:24:56 -0500 Subject: [PATCH 02/37] fix host router issue --- packages/elements/src/internals/constants/index.ts | 1 + .../machines/third-party/third-party.actors.ts | 2 +- .../nextjs/src/app-router/client/ClerkProvider.tsx | 11 +++++------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/elements/src/internals/constants/index.ts b/packages/elements/src/internals/constants/index.ts index 1ca489d3c2c..4f8c393eb80 100644 --- a/packages/elements/src/internals/constants/index.ts +++ b/packages/elements/src/internals/constants/index.ts @@ -51,6 +51,7 @@ export const ERROR_CODES = { export const ROUTING = { path: 'path', virtual: 'virtual', + hash: 'hash', } as const; export type ROUTING = (typeof ROUTING)[keyof typeof ROUTING]; diff --git a/packages/elements/src/internals/machines/third-party/third-party.actors.ts b/packages/elements/src/internals/machines/third-party/third-party.actors.ts index 519e2153f0a..050dca6ed2d 100644 --- a/packages/elements/src/internals/machines/third-party/third-party.actors.ts +++ b/packages/elements/src/internals/machines/third-party/third-party.actors.ts @@ -74,7 +74,7 @@ export const handleRedirectCallback = fromCallback { const router = useRouter(); - const pathname = usePathname(); - // eslint-disable-next-line react-hooks/rules-of-hooks -- The order doesn't differ between renders as we're checking the execution environment. - const searchParams = typeof window === 'undefined' ? new URLSearchParams() : useSearchParams(); // The window.history APIs seem to prevent Next.js from triggering a full page re-render, allowing us to // preserve internal state between steps. @@ -52,8 +49,8 @@ export const useNextRouter = (): ClerkHostRouter => { shallowPush(path: string) { canUseWindowHistoryAPIs ? window.history.pushState(null, '', path) : router.push(path, {}); }, - pathname: () => pathname, - searchParams: () => searchParams, + pathname: () => window.location.pathname, + searchParams: () => new URLSearchParams(window.location.search), }; }; @@ -113,6 +110,8 @@ export const ClientClerkProvider = (props: NextClerkProviderProps) => { const mergedProps = mergeNextClerkPropsWithEnv({ ...props, + // @ts-expect-error -- TODO: type + router: clerkRouter, routerPush: push, routerReplace: replace, }); From b3ee721e4e04f8837c4ade55b3a58ac4d08fed71 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 5 Sep 2024 17:25:58 -0500 Subject: [PATCH 03/37] new ui renderer --- package-lock.json | 1 + packages/clerk-js/package.json | 1 + packages/clerk-js/src/core/clerk.ts | 33 ++++++--- packages/clerk-js/src/ui/new/index.tsx | 88 +++++++++++++++++++++++ packages/clerk-js/src/ui/new/renderer.tsx | 76 ++++++++++++++++++++ packages/clerk-js/webpack.config.js | 3 + 6 files changed, 193 insertions(+), 9 deletions(-) create mode 100644 packages/clerk-js/src/ui/new/index.tsx create mode 100644 packages/clerk-js/src/ui/new/renderer.tsx diff --git a/package-lock.json b/package-lock.json index bcb1421a8fa..cb3df96b897 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43230,6 +43230,7 @@ "@clerk/localizations": "2.8.1", "@clerk/shared": "2.6.2", "@clerk/types": "4.19.0", + "@clerk/ui": "^0.1.8", "@coinbase/wallet-sdk": "4.0.4", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.1", diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index 720af612b3c..c258f715f9a 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -53,6 +53,7 @@ "@clerk/localizations": "2.8.1", "@clerk/shared": "2.6.2", "@clerk/types": "4.19.0", + "@clerk/ui": "^0.1.8", "@coinbase/wallet-sdk": "4.0.4", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.1", diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 0aa484da6a9..40151b1cec9 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -121,6 +121,9 @@ import { } from './resources/internal'; import { warnings } from './warnings'; +import { UI } from '../ui/new'; +import { ClerkHostRouter } from '@clerk/shared/router'; + export type ClerkCoreBroadcastChannelEvent = { type: 'signout' }; declare global { @@ -147,6 +150,8 @@ const defaultOptions: ClerkOptions = { }; export class Clerk implements ClerkInterface { + // @ts-expect-error -- TODO: ensure defined + public ui: UI; public static mountComponentRenderer?: MountComponentRenderer; public static version: string = __PKG_VERSION__; @@ -182,6 +187,7 @@ export class Clerk implements ClerkInterface { #options: ClerkOptions = {}; #pageLifecycle: ReturnType | null = null; #touchThrottledUntil = 0; + #router: ClerkHostRouter; get publishableKey(): string { return this.#publishableKey; @@ -308,6 +314,11 @@ export class Clerk implements ClerkInterface { }); } + // @ts-expect-error -- TODO: type + this.#router = this.#options.router; + // @ts-expect-error -- TODO: type + this.ui = new UI({ router: this.#options.router, clerk: this, options: this.#options }); + this.#options.allowedRedirectOrigins = createAllowedRedirectOrigins( this.#options.allowedRedirectOrigins, this.frontendApi, @@ -496,15 +507,19 @@ export class Clerk implements ClerkInterface { }; public mountSignIn = (node: HTMLDivElement, props?: SignInProps): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted({ preloadHint: 'SignIn' }).then(controls => - controls.mountComponent({ - name: 'SignIn', - appearanceKey: 'signIn', - node, - props, - }), - ); + if (props.experimental?.newComponents) { + this.ui.mount('SignIn', node, props); + } else { + this.assertComponentsReady(this.#componentControls); + void this.#componentControls.ensureMounted({ preloadHint: 'SignIn' }).then(controls => + controls.mountComponent({ + name: 'SignIn', + appearanceKey: 'signIn', + node, + props, + }), + ); + } this.telemetry?.record(eventPrebuiltComponentMounted('SignIn', props)); }; diff --git a/packages/clerk-js/src/ui/new/index.tsx b/packages/clerk-js/src/ui/new/index.tsx new file mode 100644 index 00000000000..4a68c5a2c1c --- /dev/null +++ b/packages/clerk-js/src/ui/new/index.tsx @@ -0,0 +1,88 @@ +import { createDeferredPromise } from '@clerk/shared'; +import { ClerkHostRouter } from '@clerk/shared/router'; +import { ClerkOptions, LoadedClerk } from '@clerk/types'; + +type $TODO = any; + +function createObservablePromise() { + let status = 'pending'; + + const deferred = createDeferredPromise(); + const resolve = () => { + deferred.resolve(); + status = 'resolved'; + }; + + const reject = () => { + deferred.reject(); + status = 'rejected'; + }; + + return { + status, + resolve, + reject, + promise: deferred.promise, + }; +} + +export class UI { + router: ClerkHostRouter; + clerk: LoadedClerk; + options: ClerkOptions; + componentRegistry = new Map(); + + #rendererPromise: ReturnType; + #renderer: ReturnType<(typeof import('./renderer'))['init']>; + + constructor({ router, clerk, options }: { router: ClerkHostRouter; clerk: LoadedClerk; options: ClerkOptions }) { + this.router = router; + this.clerk = clerk; + this.options = options; + + // register components + this.register('SignIn', { + load: () => + import(/* webpackChunkName: "sign-in-new" */ '@clerk/ui/sign-in').then(({ SignIn }) => ({ default: SignIn })), + }); + } + + // Mount a component from the registry + mount(componentName: string, node: HTMLElement, props: $TODO) { + const component = this.componentRegistry.get(componentName); + if (!component) { + throw new Error(`clerk/ui: Unable to find component definition for ${componentName}`); + } + + // immediately start loading the component + component.load(); + + this.renderer().then(() => { + this.#renderer.mount(this.#renderer.createElementFromComponentDefinition(component), props, node); + }); + } + + unmount(node: HTMLElement) { + this.#renderer.unmount(node); + } + + // Registers a component for rendering later + register(componentName: string, componentDefinition: $TODO) { + this.componentRegistry.set(componentName, componentDefinition); + } + + renderer() { + if (this.#rendererPromise) { + return this.#rendererPromise.promise; + } + + this.#rendererPromise = createObservablePromise(); + + import('./renderer').then(({ init }) => { + this.#renderer = init({ router: this.router, clerk: this.clerk, options: this.options }); + this.#rendererPromise.resolve(); + }); + + return this.#rendererPromise.promise; + } +} diff --git a/packages/clerk-js/src/ui/new/renderer.tsx b/packages/clerk-js/src/ui/new/renderer.tsx new file mode 100644 index 00000000000..bbd61d9785f --- /dev/null +++ b/packages/clerk-js/src/ui/new/renderer.tsx @@ -0,0 +1,76 @@ +import { ClerkInstanceContext, OptionsContext } from '@clerk/shared/react'; +import { ClerkHostRouter, ClerkHostRouterContext } from '@clerk/shared/router'; +import { ClerkOptions, LoadedClerk } from '@clerk/types'; +import { ElementType, createElement, lazy } from 'react'; +import { createPortal } from 'react-dom'; +import { Root, createRoot } from 'react-dom/client'; + +// TODO: don't import here +import '@clerk/ui/styles.css'; + +const ROOT_ELEMENT_ID = 'clerk-components-new'; + +// Initializes the react renderer +export function init({ + router, + clerk, + options, +}: { + router: ClerkHostRouter; + clerk: LoadedClerk; + options: ClerkOptions; +}) { + let renderedComponents = new Map]>(); + let rootElement = document.getElementById(ROOT_ELEMENT_ID); + let root: Root; + + if (!rootElement) { + rootElement = document.createElement('div'); + rootElement.setAttribute('id', 'clerk-components'); + document.body.appendChild(rootElement); + } + + root = createRoot(rootElement); + + function ClerkComponentContainer() { + return ( + + + + {Array.from(renderedComponents.entries()).map(([node, [element, props]]) => + createPortal(createElement(element, props), node), + )} + + + + ); + } + + function render() { + root.render(); + } + + function mount(element: ElementType, props: any, node: HTMLElement) { + renderedComponents.set(node, [element, props]); + render(); + } + + function unmount(node: HTMLElement) { + if (!renderedComponents.has(node)) { + return; + } + + renderedComponents.delete(node); + render(); + } + + function createElementFromComponentDefinition(componentDefinition: any) { + return lazy(componentDefinition.load); + } + + return { + mount, + unmount, + createElementFromComponentDefinition, + }; +} diff --git a/packages/clerk-js/webpack.config.js b/packages/clerk-js/webpack.config.js index fee716e7fe6..a19cf0e85e1 100644 --- a/packages/clerk-js/webpack.config.js +++ b/packages/clerk-js/webpack.config.js @@ -80,6 +80,9 @@ const common = ({ mode }) => { }, }, }, + experiments: { + css: true, + }, }; }; From 851f201dfbff93bdb3dd275bf4b817ef29862b09 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 5 Sep 2024 21:13:27 -0500 Subject: [PATCH 04/37] refactor component renderer, add sign up --- packages/clerk-js/src/core/clerk.ts | 27 +++++---- packages/clerk-js/src/ui/new/index.tsx | 53 ++++++++++++++---- packages/clerk-js/src/ui/new/renderer.tsx | 56 +++++++------------ packages/clerk-js/src/ui/new/types.ts | 6 ++ .../ui/src/components/sign-in/steps/start.tsx | 7 ++- .../ui/src/components/sign-up/sign-up.tsx | 19 ++++--- 6 files changed, 100 insertions(+), 68 deletions(-) create mode 100644 packages/clerk-js/src/ui/new/types.ts diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 40151b1cec9..c0d0d531eac 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -15,6 +15,7 @@ import { stripScheme, } from '@clerk/shared'; import { logger } from '@clerk/shared/logger'; +import type { ClerkHostRouter } from '@clerk/shared/router'; import { eventPrebuiltComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry'; import type { __experimental_UserVerificationModalProps, @@ -65,6 +66,7 @@ import type { } from '@clerk/types'; import type { MountComponentRenderer } from '../ui/Components'; +import { UI } from '../ui/new'; import { ALLOWED_PROTOCOLS, buildURL, @@ -121,9 +123,6 @@ import { } from './resources/internal'; import { warnings } from './warnings'; -import { UI } from '../ui/new'; -import { ClerkHostRouter } from '@clerk/shared/router'; - export type ClerkCoreBroadcastChannelEvent = { type: 'signout' }; declare global { @@ -565,15 +564,19 @@ export class Clerk implements ClerkInterface { }; public mountSignUp = (node: HTMLDivElement, props?: SignUpProps): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted({ preloadHint: 'SignUp' }).then(controls => - controls.mountComponent({ - name: 'SignUp', - appearanceKey: 'signUp', - node, - props, - }), - ); + if (props.experimental?.newComponents) { + this.ui.mount('SignUp', node, props); + } else { + this.assertComponentsReady(this.#componentControls); + void this.#componentControls.ensureMounted({ preloadHint: 'SignUp' }).then(controls => + controls.mountComponent({ + name: 'SignUp', + appearanceKey: 'signUp', + node, + props, + }), + ); + } this.telemetry?.record(eventPrebuiltComponentMounted('SignUp', props)); }; diff --git a/packages/clerk-js/src/ui/new/index.tsx b/packages/clerk-js/src/ui/new/index.tsx index 4a68c5a2c1c..d8f75c94c3b 100644 --- a/packages/clerk-js/src/ui/new/index.tsx +++ b/packages/clerk-js/src/ui/new/index.tsx @@ -1,6 +1,12 @@ import { createDeferredPromise } from '@clerk/shared'; -import { ClerkHostRouter } from '@clerk/shared/router'; -import { ClerkOptions, LoadedClerk } from '@clerk/types'; +import { ClerkInstanceContext, OptionsContext } from '@clerk/shared/react'; +import type { ClerkHostRouter } from '@clerk/shared/router'; +import { ClerkHostRouterContext } from '@clerk/shared/router'; +import type { ClerkOptions, LoadedClerk } from '@clerk/types'; +import type { ComponentType, ReactNode } from 'react'; + +import type { init } from './renderer'; +import type { ComponentDefinition } from './types'; type $TODO = any; @@ -30,10 +36,11 @@ export class UI { router: ClerkHostRouter; clerk: LoadedClerk; options: ClerkOptions; - componentRegistry = new Map(); + componentRegistry = new Map(); - #rendererPromise: ReturnType; - #renderer: ReturnType<(typeof import('./renderer'))['init']>; + #rendererPromise?: ReturnType; + #renderer?: ReturnType; + #wrapper: ComponentType<{ children: ReactNode }>; constructor({ router, clerk, options }: { router: ClerkHostRouter; clerk: LoadedClerk; options: ClerkOptions }) { this.router = router; @@ -42,9 +49,25 @@ export class UI { // register components this.register('SignIn', { + type: 'component', load: () => import(/* webpackChunkName: "sign-in-new" */ '@clerk/ui/sign-in').then(({ SignIn }) => ({ default: SignIn })), }); + this.register('SignUp', { + type: 'component', + load: () => + import(/* webpackChunkName: "sign-up-new" */ '@clerk/ui/sign-up').then(({ SignUp }) => ({ default: SignUp })), + }); + + this.#wrapper = ({ children }) => { + return ( + + + {children} + + + ); + }; } // Mount a component from the registry @@ -57,17 +80,21 @@ export class UI { // immediately start loading the component component.load(); - this.renderer().then(() => { - this.#renderer.mount(this.#renderer.createElementFromComponentDefinition(component), props, node); - }); + this.renderer() + .then(() => { + this.#renderer?.mount(this.#renderer.createElementFromComponentDefinition(component), props, node); + }) + .catch(err => { + console.error(`clerk/ui: Error mounting component ${componentName}:`, err); + }); } unmount(node: HTMLElement) { - this.#renderer.unmount(node); + this.#renderer?.unmount(node); } // Registers a component for rendering later - register(componentName: string, componentDefinition: $TODO) { + register(componentName: string, componentDefinition: ComponentDefinition) { this.componentRegistry.set(componentName, componentDefinition); } @@ -79,8 +106,10 @@ export class UI { this.#rendererPromise = createObservablePromise(); import('./renderer').then(({ init }) => { - this.#renderer = init({ router: this.router, clerk: this.clerk, options: this.options }); - this.#rendererPromise.resolve(); + this.#renderer = init({ + wrapper: this.#wrapper, + }); + this.#rendererPromise?.resolve(); }); return this.#rendererPromise.promise; diff --git a/packages/clerk-js/src/ui/new/renderer.tsx b/packages/clerk-js/src/ui/new/renderer.tsx index bbd61d9785f..0954f677baa 100644 --- a/packages/clerk-js/src/ui/new/renderer.tsx +++ b/packages/clerk-js/src/ui/new/renderer.tsx @@ -1,28 +1,19 @@ -import { ClerkInstanceContext, OptionsContext } from '@clerk/shared/react'; -import { ClerkHostRouter, ClerkHostRouterContext } from '@clerk/shared/router'; -import { ClerkOptions, LoadedClerk } from '@clerk/types'; -import { ElementType, createElement, lazy } from 'react'; -import { createPortal } from 'react-dom'; -import { Root, createRoot } from 'react-dom/client'; - // TODO: don't import here import '@clerk/ui/styles.css'; +import type { ElementType } from 'react'; +import { createElement, lazy } from 'react'; +import { createPortal } from 'react-dom'; +import { createRoot } from 'react-dom/client'; + +import type { ComponentDefinition } from './types'; + const ROOT_ELEMENT_ID = 'clerk-components-new'; // Initializes the react renderer -export function init({ - router, - clerk, - options, -}: { - router: ClerkHostRouter; - clerk: LoadedClerk; - options: ClerkOptions; -}) { - let renderedComponents = new Map]>(); +export function init({ wrapper }: { wrapper: ElementType }) { + const renderedComponents = new Map]>(); let rootElement = document.getElementById(ROOT_ELEMENT_ID); - let root: Root; if (!rootElement) { rootElement = document.createElement('div'); @@ -30,24 +21,19 @@ export function init({ document.body.appendChild(rootElement); } - root = createRoot(rootElement); - - function ClerkComponentContainer() { - return ( - - - - {Array.from(renderedComponents.entries()).map(([node, [element, props]]) => - createPortal(createElement(element, props), node), - )} - - - - ); - } + const root = createRoot(rootElement); + // (re-)renders the render wrapper, rendering any components present in the `renderedComponents` map. function render() { - root.render(); + root.render( + createElement( + wrapper, + null, + Array.from(renderedComponents.entries()).map(([node, [element, props]]) => + createPortal(createElement(element, props), node), + ), + ), + ); } function mount(element: ElementType, props: any, node: HTMLElement) { @@ -64,7 +50,7 @@ export function init({ render(); } - function createElementFromComponentDefinition(componentDefinition: any) { + function createElementFromComponentDefinition(componentDefinition: ComponentDefinition) { return lazy(componentDefinition.load); } diff --git a/packages/clerk-js/src/ui/new/types.ts b/packages/clerk-js/src/ui/new/types.ts new file mode 100644 index 00000000000..178e74f2da8 --- /dev/null +++ b/packages/clerk-js/src/ui/new/types.ts @@ -0,0 +1,6 @@ +import type { ComponentType } from 'react'; + +export interface ComponentDefinition { + type: 'component' | 'modal'; + load: () => Promise<{ default: ComponentType }>; +} diff --git a/packages/ui/src/components/sign-in/steps/start.tsx b/packages/ui/src/components/sign-in/steps/start.tsx index 924ef360f7d..20c304d381e 100644 --- a/packages/ui/src/components/sign-in/steps/start.tsx +++ b/packages/ui/src/components/sign-in/steps/start.tsx @@ -43,7 +43,10 @@ export function SignInStart() { {isGlobalLoading => { const connectionsWithSeperator = [ - , + , hasConnection && hasIdentifier ? {t('dividerText')} : null, ]; return ( @@ -156,7 +159,7 @@ export function SignInStart() { // up the state based on `isSubmitting` passkeyEnabled ? ( - {isSubmitting => { + {() => { return ( - - - - {/* */} - + + + + + + {/* */} + + ); } From 371f6ee05376cfb3f6bc631928583675dc3c40cd Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 5 Sep 2024 21:14:23 -0500 Subject: [PATCH 05/37] remove unused function --- packages/clerk-js/src/ui/new/index.tsx | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/packages/clerk-js/src/ui/new/index.tsx b/packages/clerk-js/src/ui/new/index.tsx index d8f75c94c3b..f0d66810f8b 100644 --- a/packages/clerk-js/src/ui/new/index.tsx +++ b/packages/clerk-js/src/ui/new/index.tsx @@ -10,35 +10,13 @@ import type { ComponentDefinition } from './types'; type $TODO = any; -function createObservablePromise() { - let status = 'pending'; - - const deferred = createDeferredPromise(); - const resolve = () => { - deferred.resolve(); - status = 'resolved'; - }; - - const reject = () => { - deferred.reject(); - status = 'rejected'; - }; - - return { - status, - resolve, - reject, - promise: deferred.promise, - }; -} - export class UI { router: ClerkHostRouter; clerk: LoadedClerk; options: ClerkOptions; componentRegistry = new Map(); - #rendererPromise?: ReturnType; + #rendererPromise?: ReturnType; #renderer?: ReturnType; #wrapper: ComponentType<{ children: ReactNode }>; @@ -103,7 +81,7 @@ export class UI { return this.#rendererPromise.promise; } - this.#rendererPromise = createObservablePromise(); + this.#rendererPromise = createDeferredPromise(); import('./renderer').then(({ init }) => { this.#renderer = init({ From b8d014faccbe9682bad0eee114e9dcd6b42adda6 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 5 Sep 2024 21:18:28 -0500 Subject: [PATCH 06/37] add another comment --- packages/clerk-js/src/ui/new/renderer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/clerk-js/src/ui/new/renderer.tsx b/packages/clerk-js/src/ui/new/renderer.tsx index 0954f677baa..5ffbe18d87c 100644 --- a/packages/clerk-js/src/ui/new/renderer.tsx +++ b/packages/clerk-js/src/ui/new/renderer.tsx @@ -24,6 +24,7 @@ export function init({ wrapper }: { wrapper: ElementType }) { const root = createRoot(rootElement); // (re-)renders the render wrapper, rendering any components present in the `renderedComponents` map. + // React's render function retains state, so it's safe to call multiple times as additional components are mounted and unmounted. function render() { root.render( createElement( From f0c6f5147b571d4c278cf8bfd413c3eac6c501a6 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Fri, 6 Sep 2024 21:51:21 -0500 Subject: [PATCH 07/37] tweak webpack chunk settings --- packages/clerk-js/src/ui/new/index.tsx | 8 ++++++-- packages/clerk-js/webpack.config.js | 18 ++++++++++++++++++ packages/ui/package.json | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/new/index.tsx b/packages/clerk-js/src/ui/new/index.tsx index f0d66810f8b..08d3cbd3daa 100644 --- a/packages/clerk-js/src/ui/new/index.tsx +++ b/packages/clerk-js/src/ui/new/index.tsx @@ -29,12 +29,16 @@ export class UI { this.register('SignIn', { type: 'component', load: () => - import(/* webpackChunkName: "sign-in-new" */ '@clerk/ui/sign-in').then(({ SignIn }) => ({ default: SignIn })), + import(/* webpackChunkName: "rebuild--sign-in" */ '@clerk/ui/sign-in').then(({ SignIn }) => ({ + default: SignIn, + })), }); this.register('SignUp', { type: 'component', load: () => - import(/* webpackChunkName: "sign-up-new" */ '@clerk/ui/sign-up').then(({ SignUp }) => ({ default: SignUp })), + import(/* webpackChunkName: "rebuild--sign-up" */ '@clerk/ui/sign-up').then(({ SignUp }) => ({ + default: SignUp, + })), }); this.#wrapper = ({ children }) => { diff --git a/packages/clerk-js/webpack.config.js b/packages/clerk-js/webpack.config.js index a19cf0e85e1..2045d3713f2 100644 --- a/packages/clerk-js/webpack.config.js +++ b/packages/clerk-js/webpack.config.js @@ -26,6 +26,7 @@ const variantToSourceFile = { /** @returns { import('webpack').Configuration } */ const common = ({ mode }) => { + /** @type { import('webpack').Configuration } */ return { mode, resolve: { @@ -77,6 +78,21 @@ const common = ({ mode }) => { name: 'vendors', priority: -10, }, + commonNew: { + minChunks: 2, + name: 'common-new', + chunks(chunk) { + return chunk.name?.startsWith('rebuild--'); + }, + priority: 0, + }, + react: { + chunks: 'all', + test: /[\\/]node_modules[\\/](react-dom|scheduler)[\\/]/, + name: 'framework', + priority: 40, + enforce: true, + }, }, }, }, @@ -167,6 +183,8 @@ const commonForProd = () => { new TerserPlugin({ terserOptions: { compress: { + unused: true, + dead_code: true, passes: 2, }, mangle: { diff --git a/packages/ui/package.json b/packages/ui/package.json index f1df98613f7..a75544bcbca 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -13,6 +13,7 @@ "url": "git+https://github.com/clerk/javascript.git", "directory": "packages/ui" }, + "sideEffects": false, "exports": { "./contexts": { "import": { From 2fdd33e1968207a750c9418e1e502ff26a749af2 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 10 Sep 2024 13:42:14 -0400 Subject: [PATCH 08/37] fix bring back isSubmitting boolean --- packages/ui/src/components/sign-in/steps/start.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/sign-in/steps/start.tsx b/packages/ui/src/components/sign-in/steps/start.tsx index 0c47d62fcd3..c6656e4a936 100644 --- a/packages/ui/src/components/sign-in/steps/start.tsx +++ b/packages/ui/src/components/sign-in/steps/start.tsx @@ -165,7 +165,7 @@ export function SignInStart() { // up the state based on `isSubmitting` passkeyEnabled ? ( - {() => { + {isSubmitting => { return ( Date: Tue, 10 Sep 2024 15:00:44 -0400 Subject: [PATCH 09/37] update sideEffects to enable loading css file --- packages/ui/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index a75544bcbca..bd9390c2f9d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -13,7 +13,9 @@ "url": "git+https://github.com/clerk/javascript.git", "directory": "packages/ui" }, - "sideEffects": false, + "sideEffects": [ + "*.css" + ], "exports": { "./contexts": { "import": { From f0006898fcf6c32a928710f55c195184e30cb2a5 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:18:08 -0700 Subject: [PATCH 10/37] feat(clerk-js,types,shared): Add necessary types for new UI --- packages/clerk-js/src/core/clerk.ts | 32 +++++++++++++++----------- packages/clerk-js/src/ui/new/index.tsx | 19 +++++++++++++-- packages/shared/src/router.ts | 3 +-- packages/shared/src/router/router.ts | 18 ++++----------- packages/shared/src/router/types.ts | 1 - packages/types/src/clerk.ts | 24 +++++++++++++++++++ packages/types/src/index.ts | 1 + packages/types/src/router.ts | 14 +++++++++++ 8 files changed, 80 insertions(+), 32 deletions(-) delete mode 100644 packages/shared/src/router/types.ts create mode 100644 packages/types/src/router.ts diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index c0d0d531eac..44840cc3cba 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -15,7 +15,6 @@ import { stripScheme, } from '@clerk/shared'; import { logger } from '@clerk/shared/logger'; -import type { ClerkHostRouter } from '@clerk/shared/router'; import { eventPrebuiltComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry'; import type { __experimental_UserVerificationModalProps, @@ -39,6 +38,7 @@ import type { HandleOAuthCallbackParams, InstanceType, ListenerCallback, + LoadedClerk, NavigateOptions, OrganizationListProps, OrganizationProfileProps, @@ -148,9 +148,14 @@ const defaultOptions: ClerkOptions = { signUpForceRedirectUrl: undefined, }; +function assertClerkIsLoaded(clerk: ClerkInterface): asserts clerk is LoadedClerk { + if (!clerk.client) { + throw new Error(`Clerk: Failed to load client`); + } +} + export class Clerk implements ClerkInterface { - // @ts-expect-error -- TODO: ensure defined - public ui: UI; + public __experimental_ui?: UI; public static mountComponentRenderer?: MountComponentRenderer; public static version: string = __PKG_VERSION__; @@ -186,7 +191,6 @@ export class Clerk implements ClerkInterface { #options: ClerkOptions = {}; #pageLifecycle: ReturnType | null = null; #touchThrottledUntil = 0; - #router: ClerkHostRouter; get publishableKey(): string { return this.#publishableKey; @@ -313,11 +317,6 @@ export class Clerk implements ClerkInterface { }); } - // @ts-expect-error -- TODO: type - this.#router = this.#options.router; - // @ts-expect-error -- TODO: type - this.ui = new UI({ router: this.#options.router, clerk: this, options: this.#options }); - this.#options.allowedRedirectOrigins = createAllowedRedirectOrigins( this.#options.allowedRedirectOrigins, this.frontendApi, @@ -328,6 +327,13 @@ export class Clerk implements ClerkInterface { } else { this.#loaded = await this.#loadInNonStandardBrowser(); } + assertClerkIsLoaded(this); + + this.__experimental_ui = new UI({ + router: this.#options.__experimental_router, + clerk: this, + options: this.#options, + }); }; public signOut: SignOut = async (callbackOrOptions?: SignOutCallback | SignOutOptions, options?: SignOutOptions) => { @@ -506,8 +512,8 @@ export class Clerk implements ClerkInterface { }; public mountSignIn = (node: HTMLDivElement, props?: SignInProps): void => { - if (props.experimental?.newComponents) { - this.ui.mount('SignIn', node, props); + if (props && props.__experimental?.newComponents && this.__experimental_ui) { + this.__experimental_ui.mount('SignIn', node, props); } else { this.assertComponentsReady(this.#componentControls); void this.#componentControls.ensureMounted({ preloadHint: 'SignIn' }).then(controls => @@ -564,8 +570,8 @@ export class Clerk implements ClerkInterface { }; public mountSignUp = (node: HTMLDivElement, props?: SignUpProps): void => { - if (props.experimental?.newComponents) { - this.ui.mount('SignUp', node, props); + if (props && props.__experimental?.newComponents && this.__experimental_ui) { + this.__experimental_ui.mount('SignUp', node, props); } else { this.assertComponentsReady(this.#componentControls); void this.#componentControls.ensureMounted({ preloadHint: 'SignUp' }).then(controls => diff --git a/packages/clerk-js/src/ui/new/index.tsx b/packages/clerk-js/src/ui/new/index.tsx index 08d3cbd3daa..df877127f46 100644 --- a/packages/clerk-js/src/ui/new/index.tsx +++ b/packages/clerk-js/src/ui/new/index.tsx @@ -10,8 +10,14 @@ import type { ComponentDefinition } from './types'; type $TODO = any; +function assertRouter(router: ClerkHostRouter | undefined): asserts router is ClerkHostRouter { + if (!router) { + throw new Error(`Clerk: Attempted to use functionality that requires the "router" option to be provided to Clerk.`); + } +} + export class UI { - router: ClerkHostRouter; + router?: ClerkHostRouter; clerk: LoadedClerk; options: ClerkOptions; componentRegistry = new Map(); @@ -20,7 +26,15 @@ export class UI { #renderer?: ReturnType; #wrapper: ComponentType<{ children: ReactNode }>; - constructor({ router, clerk, options }: { router: ClerkHostRouter; clerk: LoadedClerk; options: ClerkOptions }) { + constructor({ + router, + clerk, + options, + }: { + router: ClerkHostRouter | undefined; + clerk: LoadedClerk; + options: ClerkOptions; + }) { this.router = router; this.clerk = clerk; this.options = options; @@ -42,6 +56,7 @@ export class UI { }); this.#wrapper = ({ children }) => { + assertRouter(this.router); return ( diff --git a/packages/shared/src/router.ts b/packages/shared/src/router.ts index 32dc0abf2ed..b842f0ad05a 100644 --- a/packages/shared/src/router.ts +++ b/packages/shared/src/router.ts @@ -1,5 +1,4 @@ -export { type ClerkRouter, type ClerkHostRouter, createClerkRouter } from './router/router'; -export { type RoutingMode } from './router/types'; +export { type ClerkRouter, type ClerkHostRouter, type RoutingMode, createClerkRouter } from './router/router'; export { Router, useClerkRouter, diff --git a/packages/shared/src/router/router.ts b/packages/shared/src/router/router.ts index dca2e40946c..e8c16969228 100644 --- a/packages/shared/src/router/router.ts +++ b/packages/shared/src/router/router.ts @@ -1,21 +1,9 @@ +import type { ClerkHostRouter, RoutingMode } from '@clerk/types'; + import { isAbsoluteUrl, withLeadingSlash, withoutTrailingSlash } from '../url'; -import type { RoutingMode } from './types'; export const PRESERVED_QUERYSTRING_PARAMS = ['after_sign_in_url', 'after_sign_up_url', 'redirect_url']; -/** - * This type represents a generic router interface that Clerk relies on to interact with the host router. - */ -export type ClerkHostRouter = { - readonly mode: RoutingMode; - readonly name: string; - pathname: () => string; - push: (path: string) => void; - replace: (path: string) => void; - searchParams: () => URLSearchParams; - shallowPush: (path: string) => void; -}; - /** * Internal Clerk router, used by Clerk components to interact with the host's router. */ @@ -156,3 +144,5 @@ export function createClerkRouter(router: ClerkHostRouter, basePath: string = '/ basePath: normalizedBasePath, }; } + +export type { ClerkHostRouter, RoutingMode }; diff --git a/packages/shared/src/router/types.ts b/packages/shared/src/router/types.ts deleted file mode 100644 index d0f948e71d0..00000000000 --- a/packages/shared/src/router/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type RoutingMode = 'path' | 'virtual'; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 5541f589abe..64741481a46 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -32,6 +32,7 @@ import type { SignUpFallbackRedirectUrl, SignUpForceRedirectUrl, } from './redirects'; +import type { ClerkHostRouter } from './router'; import type { ActiveSessionResource } from './session'; import type { __experimental_SessionVerificationLevel } from './sessionVerification'; import type { SignInResource } from './signIn'; @@ -712,6 +713,11 @@ export type ClerkOptions = ClerkOptionsNavigation & }, Record >; + + /** + * [EXPERIMENTAL] Provide the underlying host router, required for the new experimental UI components. + */ + __experimental_router?: ClerkHostRouter; }; export interface NavigateOptions { @@ -847,6 +853,15 @@ export type SignInProps = RoutingOptions & { * Initial values that are used to prefill the sign in form. */ initialValues?: SignInInitialValues; + /** + * Enable experimental flags to gain access to new features. These flags are not guaranteed to be stable and may change drastically in between patch or minor versions. + */ + __experimental?: Autocomplete< + { + newComponents: boolean; + }, + Record + >; } & TransferableOption & SignUpForceRedirectUrl & SignUpFallbackRedirectUrl & @@ -948,6 +963,15 @@ export type SignUpProps = RoutingOptions & { * Initial values that are used to prefill the sign up form. */ initialValues?: SignUpInitialValues; + /** + * Enable experimental flags to gain access to new features. These flags are not guaranteed to be stable and may change drastically in between patch or minor versions. + */ + __experimental?: Autocomplete< + { + newComponents: boolean; + }, + Record + >; } & SignInFallbackRedirectUrl & SignInForceRedirectUrl & LegacyRedirectProps & diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 547ac4ae763..e8ac5206d85 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -36,6 +36,7 @@ export * from './phoneNumber'; export * from './redirects'; export * from './resource'; export * from './role'; +export * from './router'; export * from './saml'; export * from './samlAccount'; export * from './session'; diff --git a/packages/types/src/router.ts b/packages/types/src/router.ts new file mode 100644 index 00000000000..b159142ac7a --- /dev/null +++ b/packages/types/src/router.ts @@ -0,0 +1,14 @@ +export type RoutingMode = 'path' | 'virtual'; + +/** + * This type represents a generic router interface that Clerk relies on to interact with the host router. + */ +export type ClerkHostRouter = { + readonly mode: RoutingMode; + readonly name: string; + pathname: () => string; + push: (path: string) => void; + replace: (path: string) => void; + searchParams: () => URLSearchParams; + shallowPush: (path: string) => void; +}; From 9866b165670754b1467eea6634eb29cb8234f6cb Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:28:53 -0700 Subject: [PATCH 11/37] chore(repo): Add changeset --- .changeset/nervous-guests-guess.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/nervous-guests-guess.md diff --git a/.changeset/nervous-guests-guess.md b/.changeset/nervous-guests-guess.md new file mode 100644 index 00000000000..2e2d2d3586a --- /dev/null +++ b/.changeset/nervous-guests-guess.md @@ -0,0 +1,9 @@ +--- +"@clerk/clerk-js": minor +"@clerk/elements": minor +"@clerk/nextjs": minor +"@clerk/shared": minor +"@clerk/types": minor +--- + +Add experimental support for new UI components From 812035a656a507f8cbccc3351f5b1bf0e7dd3e31 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:47:48 -0700 Subject: [PATCH 12/37] fix(clerk-js): Add dependency on @clerk/ui --- package-lock.json | 1 + packages/clerk-js/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index a53aeb04077..07aeb0255cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40541,6 +40541,7 @@ "@clerk/localizations": "3.0.4", "@clerk/shared": "2.8.1", "@clerk/types": "4.21.0", + "@clerk/ui": "0.1.9", "@coinbase/wallet-sdk": "4.0.4", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.1", diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index ead039668dd..611ed43e0c7 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -53,6 +53,7 @@ "@clerk/localizations": "3.0.4", "@clerk/shared": "2.8.1", "@clerk/types": "4.21.0", + "@clerk/ui": "0.1.9", "@coinbase/wallet-sdk": "4.0.4", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.1", From da7ba539b985b6d564fe45ecf681943bf8f59cd9 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:00:00 -0700 Subject: [PATCH 13/37] chore(clerk-js): Temporarily increase bundlewatch limits --- packages/clerk-js/bundlewatch.config.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index bbb0e027c5c..e9420516c2c 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,7 +1,7 @@ { "files": [ - { "path": "./dist/clerk.browser.js", "maxSize": "64kB" }, - { "path": "./dist/clerk.headless.js", "maxSize": "43kB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "68kB" }, + { "path": "./dist/clerk.headless.js", "maxSize": "60kB" }, { "path": "./dist/ui-common*.js", "maxSize": "86KB" }, { "path": "./dist/vendors*.js", "maxSize": "70KB" }, { "path": "./dist/coinbase*.js", "maxSize": "58KB" }, @@ -15,6 +15,6 @@ { "path": "./dist/userbutton*.js", "maxSize": "5KB" }, { "path": "./dist/userprofile*.js", "maxSize": "15KB" }, { "path": "./dist/userverification*.js", "maxSize": "5KB" }, - { "path": "./dist/onetap*.js", "maxSize": "1KB" } + { "path": "./dist/onetap*.js", "maxSize": "2KB" } ] } From 3fe0bda4abd9bc5b30990e2d0c3af9d249ae034b Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:05:18 -0700 Subject: [PATCH 14/37] chore(clerk-js): Temporarily increase bundlewatch limits --- packages/clerk-js/bundlewatch.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index e9420516c2c..3d136638d77 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "./dist/clerk.browser.js", "maxSize": "68kB" }, { "path": "./dist/clerk.headless.js", "maxSize": "60kB" }, - { "path": "./dist/ui-common*.js", "maxSize": "86KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "100KB" }, { "path": "./dist/vendors*.js", "maxSize": "70KB" }, { "path": "./dist/coinbase*.js", "maxSize": "58KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, From 9542bab0f64a36b89ce133f616fce4c61fdd9cb3 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:37:14 -0700 Subject: [PATCH 15/37] fix(clerk-js): Ensure CJS script doesn't contain ESM code --- packages/clerk-js/webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/clerk-js/webpack.config.js b/packages/clerk-js/webpack.config.js index 2045d3713f2..4d8e11bdffa 100644 --- a/packages/clerk-js/webpack.config.js +++ b/packages/clerk-js/webpack.config.js @@ -271,6 +271,7 @@ const prodConfig = ({ mode }) => { output: { filename: '[name].js', libraryTarget: 'commonjs', + scriptType: 'text/javascript', }, }); From a75375fd46b9cb35aa8fc7c9571d7a9ea16edbf1 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:47:51 -0700 Subject: [PATCH 16/37] fix(ui): Mark package as public --- packages/ui/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index eab3d1e320f..6f009787b5d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,6 @@ { "name": "@clerk/ui", "version": "0.1.9", - "private": true, "license": "MIT", "author": { "name": "Clerk, Inc.", From 7f71197b2d99589b6f0f8a3532024f9a45fb0b1b Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:50:45 -0700 Subject: [PATCH 17/37] fix(nextjs): Pass correct router option --- packages/nextjs/src/app-router/client/ClerkProvider.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index f6bcd4af850..fa69ff49271 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -110,8 +110,7 @@ export const ClientClerkProvider = (props: NextClerkProviderProps) => { const mergedProps = mergeNextClerkPropsWithEnv({ ...props, - // @ts-expect-error -- TODO: type - router: clerkRouter, + __experimental_router: clerkRouter, routerPush: push, routerReplace: replace, }); From 31d749cbb0da83f0056bc9e296a4edef533b0f7c Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:14:32 -0700 Subject: [PATCH 18/37] fix(clerk-js): Emit CJS chunks as CJS --- packages/clerk-js/webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/clerk-js/webpack.config.js b/packages/clerk-js/webpack.config.js index 4d8e11bdffa..f4da21362d1 100644 --- a/packages/clerk-js/webpack.config.js +++ b/packages/clerk-js/webpack.config.js @@ -271,6 +271,7 @@ const prodConfig = ({ mode }) => { output: { filename: '[name].js', libraryTarget: 'commonjs', + chunkFormat: 'commonjs', scriptType: 'text/javascript', }, }); From 86419e079f4da63096c7a6eaab0dbe5914b125cf Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:00:33 -0700 Subject: [PATCH 19/37] fix(clerk-js): Only instantiate new UI if Clerk is loaded --- packages/clerk-js/src/core/clerk.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 44840cc3cba..4e1c443c87e 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -148,10 +148,8 @@ const defaultOptions: ClerkOptions = { signUpForceRedirectUrl: undefined, }; -function assertClerkIsLoaded(clerk: ClerkInterface): asserts clerk is LoadedClerk { - if (!clerk.client) { - throw new Error(`Clerk: Failed to load client`); - } +function clerkIsLoaded(clerk: ClerkInterface): clerk is LoadedClerk { + return !!clerk.client; } export class Clerk implements ClerkInterface { @@ -327,13 +325,14 @@ export class Clerk implements ClerkInterface { } else { this.#loaded = await this.#loadInNonStandardBrowser(); } - assertClerkIsLoaded(this); - this.__experimental_ui = new UI({ - router: this.#options.__experimental_router, - clerk: this, - options: this.#options, - }); + if (clerkIsLoaded(this)) { + this.__experimental_ui = new UI({ + router: this.#options.__experimental_router, + clerk: this, + options: this.#options, + }); + } }; public signOut: SignOut = async (callbackOrOptions?: SignOutCallback | SignOutOptions, options?: SignOutOptions) => { From e264672f226bc5b27e2cff12e6559edd89c25857 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:26:31 -0700 Subject: [PATCH 20/37] fix(clerk-js): Remove scriptType config --- packages/clerk-js/webpack.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/clerk-js/webpack.config.js b/packages/clerk-js/webpack.config.js index f4da21362d1..9a3fcdd461c 100644 --- a/packages/clerk-js/webpack.config.js +++ b/packages/clerk-js/webpack.config.js @@ -272,7 +272,6 @@ const prodConfig = ({ mode }) => { filename: '[name].js', libraryTarget: 'commonjs', chunkFormat: 'commonjs', - scriptType: 'text/javascript', }, }); From 4e84b2585db965881f724336784bb0eacafe19b4 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 25 Sep 2024 12:52:01 -0700 Subject: [PATCH 21/37] fix(clerk-js): Exclude ui and elements package from ui-common chunk --- packages/clerk-js/bundlewatch.config.json | 4 ++-- packages/clerk-js/webpack.config.js | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 3d136638d77..b94f9a5ada1 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,8 +1,8 @@ { "files": [ { "path": "./dist/clerk.browser.js", "maxSize": "68kB" }, - { "path": "./dist/clerk.headless.js", "maxSize": "60kB" }, - { "path": "./dist/ui-common*.js", "maxSize": "100KB" }, + { "path": "./dist/clerk.headless.js", "maxSize": "43kB" }, + { "path": "./dist/ui-common*.js", "maxSize": "86KB" }, { "path": "./dist/vendors*.js", "maxSize": "70KB" }, { "path": "./dist/coinbase*.js", "maxSize": "58KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, diff --git a/packages/clerk-js/webpack.config.js b/packages/clerk-js/webpack.config.js index 9a3fcdd461c..c7d0af1442a 100644 --- a/packages/clerk-js/webpack.config.js +++ b/packages/clerk-js/webpack.config.js @@ -70,7 +70,11 @@ const common = ({ mode }) => { minChunks: 1, name: 'ui-common', priority: -20, - test: module => module.resource && !module.resource.includes('/ui/components'), + test: module => + module.resource && + !module.resource.includes('/ui/components') && + !module.resource.includes('packages/elements') && + !module.resource.includes('packages/ui'), }, defaultVendors: { minChunks: 1, From 4b50d86ca45a610361bd8bb01748c1ce1e0a82a2 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:13:24 -0700 Subject: [PATCH 22/37] fix(clerk-js): Consolidate wrapper and renderer into single chunk --- packages/clerk-js/src/ui/new/index.tsx | 20 +++-------------- packages/clerk-js/src/ui/new/renderer.tsx | 26 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/clerk-js/src/ui/new/index.tsx b/packages/clerk-js/src/ui/new/index.tsx index df877127f46..0d9b143abd2 100644 --- a/packages/clerk-js/src/ui/new/index.tsx +++ b/packages/clerk-js/src/ui/new/index.tsx @@ -1,9 +1,6 @@ import { createDeferredPromise } from '@clerk/shared'; -import { ClerkInstanceContext, OptionsContext } from '@clerk/shared/react'; import type { ClerkHostRouter } from '@clerk/shared/router'; -import { ClerkHostRouterContext } from '@clerk/shared/router'; import type { ClerkOptions, LoadedClerk } from '@clerk/types'; -import type { ComponentType, ReactNode } from 'react'; import type { init } from './renderer'; import type { ComponentDefinition } from './types'; @@ -24,7 +21,6 @@ export class UI { #rendererPromise?: ReturnType; #renderer?: ReturnType; - #wrapper: ComponentType<{ children: ReactNode }>; constructor({ router, @@ -54,17 +50,6 @@ export class UI { default: SignUp, })), }); - - this.#wrapper = ({ children }) => { - assertRouter(this.router); - return ( - - - {children} - - - ); - }; } // Mount a component from the registry @@ -102,9 +87,10 @@ export class UI { this.#rendererPromise = createDeferredPromise(); - import('./renderer').then(({ init }) => { + import('./renderer').then(({ init, wrapperInit }) => { + assertRouter(this.router); this.#renderer = init({ - wrapper: this.#wrapper, + wrapper: wrapperInit({ clerk: this.clerk, options: this.options, router: this.router }), }); this.#rendererPromise?.resolve(); }); diff --git a/packages/clerk-js/src/ui/new/renderer.tsx b/packages/clerk-js/src/ui/new/renderer.tsx index 5ffbe18d87c..a90a5dc7504 100644 --- a/packages/clerk-js/src/ui/new/renderer.tsx +++ b/packages/clerk-js/src/ui/new/renderer.tsx @@ -1,7 +1,11 @@ // TODO: don't import here import '@clerk/ui/styles.css'; -import type { ElementType } from 'react'; +import { ClerkInstanceContext, OptionsContext } from '@clerk/shared/react'; +import type { ClerkHostRouter } from '@clerk/shared/router'; +import { ClerkHostRouterContext } from '@clerk/shared/router'; +import type { ClerkOptions, LoadedClerk } from '@clerk/types'; +import type { ElementType, ReactNode } from 'react'; import { createElement, lazy } from 'react'; import { createPortal } from 'react-dom'; import { createRoot } from 'react-dom/client'; @@ -10,6 +14,26 @@ import type { ComponentDefinition } from './types'; const ROOT_ELEMENT_ID = 'clerk-components-new'; +export function wrapperInit({ + clerk, + options, + router, +}: { + clerk: LoadedClerk; + options: ClerkOptions; + router: ClerkHostRouter; +}) { + return function Wrapper({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); + }; +} + // Initializes the react renderer export function init({ wrapper }: { wrapper: ElementType }) { const renderedComponents = new Map]>(); From 368db81d213f981ccee3c1f684c8196abce6fa80 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:20:39 -0700 Subject: [PATCH 23/37] fix(clerk-js): Remove duplicate dependencies --- packages/clerk-js/package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index a5a0eecc6c1..e176f645d46 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -50,9 +50,6 @@ }, "browserslist": "last 2 versions, ios_saf > 12, Safari > 12, > 1%, not dead, not ie > 0", "dependencies": { - "@clerk/localizations": "3.1.1", - "@clerk/shared": "2.8.5", - "@clerk/types": "4.24.0", "@clerk/localizations": "3.1.2", "@clerk/shared": "2.9.0", "@clerk/types": "4.25.0", From b40c29385a8916b39318fbe1f793d5ecb3d16d5d Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:47:59 -0700 Subject: [PATCH 24/37] fix(clerk-js): Add types for mount call --- packages/clerk-js/src/ui/new/index.tsx | 6 ++---- packages/clerk-js/src/ui/new/types.ts | 6 ++++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/ui/new/index.tsx b/packages/clerk-js/src/ui/new/index.tsx index 0d9b143abd2..42b8614102f 100644 --- a/packages/clerk-js/src/ui/new/index.tsx +++ b/packages/clerk-js/src/ui/new/index.tsx @@ -3,9 +3,7 @@ import type { ClerkHostRouter } from '@clerk/shared/router'; import type { ClerkOptions, LoadedClerk } from '@clerk/types'; import type { init } from './renderer'; -import type { ComponentDefinition } from './types'; - -type $TODO = any; +import type { ClerkNewComponents, ComponentDefinition } from './types'; function assertRouter(router: ClerkHostRouter | undefined): asserts router is ClerkHostRouter { if (!router) { @@ -53,7 +51,7 @@ export class UI { } // Mount a component from the registry - mount(componentName: string, node: HTMLElement, props: $TODO) { + mount(componentName: C, node: HTMLElement, props: ClerkNewComponents[C]): void { const component = this.componentRegistry.get(componentName); if (!component) { throw new Error(`clerk/ui: Unable to find component definition for ${componentName}`); diff --git a/packages/clerk-js/src/ui/new/types.ts b/packages/clerk-js/src/ui/new/types.ts index 178e74f2da8..ef5a3281961 100644 --- a/packages/clerk-js/src/ui/new/types.ts +++ b/packages/clerk-js/src/ui/new/types.ts @@ -1,6 +1,12 @@ +import type { SignInProps, SignUpProps } from '@clerk/types'; import type { ComponentType } from 'react'; export interface ComponentDefinition { type: 'component' | 'modal'; load: () => Promise<{ default: ComponentType }>; } + +export type ClerkNewComponents = { + SignIn: SignInProps; + SignUp: SignUpProps; +}; From fc3f448ec139d3cf3217128a47bc3f46976620b0 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:49:05 -0700 Subject: [PATCH 25/37] fix(clerk-js): Remove duplicate imports --- packages/ui/src/components/sign-up/sign-up.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ui/src/components/sign-up/sign-up.tsx b/packages/ui/src/components/sign-up/sign-up.tsx index 04f3dbc9d8a..15d59b39f2f 100644 --- a/packages/ui/src/components/sign-up/sign-up.tsx +++ b/packages/ui/src/components/sign-up/sign-up.tsx @@ -5,8 +5,7 @@ import { SignUpContinue } from '~/components/sign-up/steps/continue'; import { SignUpStart } from '~/components/sign-up/steps/start'; // import { SignUpStatus } from '~/components/sign-up/steps/status'; import { SignUpVerifications } from '~/components/sign-up/steps/verifications'; -import type { Appearance } from '~/contexts'; -import { AppearanceProvider } from '~/contexts'; +import { type Appearance, AppearanceProvider } from '~/contexts'; export function SignUp({ appearance, ...props }: { appearance?: Appearance } & SignUpProps) { return ( From 477ee0550c937985be7b6843ad6c0dd7bebdcf0b Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:12:06 -0700 Subject: [PATCH 26/37] fix(clerk-js): Pass __experimental_router to pages router provider --- packages/nextjs/src/pages/ClerkProvider.tsx | 38 ++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/pages/ClerkProvider.tsx b/packages/nextjs/src/pages/ClerkProvider.tsx index 0f7c71d3f2d..00253fd7630 100644 --- a/packages/nextjs/src/pages/ClerkProvider.tsx +++ b/packages/nextjs/src/pages/ClerkProvider.tsx @@ -1,6 +1,7 @@ import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react'; // Override Clerk React error thrower to show that errors come from @clerk/nextjs import { setClerkJsLoadingErrorPackageName, setErrorThrowerOptions } from '@clerk/clerk-react/internal'; +import type { ClerkHostRouter } from '@clerk/shared/router'; import { useRouter } from 'next/router'; import React from 'react'; @@ -15,9 +16,39 @@ import { removeBasePath } from '../utils/removeBasePath'; setErrorThrowerOptions({ packageName: PACKAGE_NAME }); setClerkJsLoadingErrorPackageName(PACKAGE_NAME); +// The version that Next added support for the window.history.pushState and replaceState APIs. +// ref: https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate +export const NEXT_WINDOW_HISTORY_SUPPORT_VERSION = '14.1.0'; + +/** + * Clerk router integration with Next.js's router. + */ +export const useNextRouter = (): ClerkHostRouter => { + const router = useRouter(); + + // The window.history APIs seem to prevent Next.js from triggering a full page re-render, allowing us to + // preserve internal state between steps. + const canUseWindowHistoryAPIs = + typeof window !== 'undefined' && window.next && window.next.version >= NEXT_WINDOW_HISTORY_SUPPORT_VERSION; + + return { + mode: 'path', + name: 'NextRouter', + push: (path: string) => router.push(path), + replace: (path: string) => + canUseWindowHistoryAPIs ? window.history.replaceState(null, '', path) : router.replace(path), + shallowPush(path: string) { + canUseWindowHistoryAPIs ? window.history.pushState(null, '', path) : router.push(path, {}); + }, + pathname: () => window.location.pathname, + searchParams: () => new URLSearchParams(window.location.search), + }; +}; + export function ClerkProvider({ children, ...props }: NextClerkProviderProps): JSX.Element { const { __unstable_invokeMiddlewareOnAuthStateChange = true } = props; const { push, replace } = useRouter(); + const clerkRouter = useNextRouter(); ReactClerkProvider.displayName = 'ReactClerkProvider'; useSafeLayoutEffect(() => { @@ -37,7 +68,12 @@ export function ClerkProvider({ children, ...props }: NextClerkProviderProps): J const navigate = (to: string) => push(removeBasePath(to)); const replaceNavigate = (to: string) => replace(removeBasePath(to)); - const mergedProps = mergeNextClerkPropsWithEnv({ ...props, routerPush: navigate, routerReplace: replaceNavigate }); + const mergedProps = mergeNextClerkPropsWithEnv({ + ...props, + __experimental_router: clerkRouter, + routerPush: navigate, + routerReplace: replaceNavigate, + }); // ClerkProvider automatically injects __clerk_ssr_state // getAuth returns a user-facing authServerSideProps that hides __clerk_ssr_state // @ts-expect-error initialState is hidden from the types as it's a private prop From 860897cbdccad957b9d629f735702e4ba5ad4e3c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 3 Oct 2024 10:27:40 -0400 Subject: [PATCH 27/37] feat(ui): Add `` component (#4277) Co-authored-by: panteliselef --- package-lock.json | 2 +- packages/ui/package.json | 8 +-- packages/ui/src/common/password-field.tsx | 45 ++++++++++------- packages/ui/src/common/router-link.tsx | 28 +++++++++++ .../ui/src/components/sign-in/steps/start.tsx | 50 ++++++++++++++++++- .../src/components/sign-up/steps/continue.tsx | 12 ++++- .../ui/src/components/sign-up/steps/start.tsx | 10 +++- 7 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 packages/ui/src/common/router-link.tsx diff --git a/package-lock.json b/package-lock.json index cec8749a398..e341029a900 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44923,6 +44923,7 @@ "@clerk/localizations": "3.1.2", "@clerk/shared": "2.9.0", "@clerk/types": "4.25.0", + "@clerk/ui": "0.1.9", "@coinbase/wallet-sdk": "4.0.4", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.1", @@ -49166,7 +49167,6 @@ "version": "0.1.9", "license": "MIT", "dependencies": { - "@clerk/clerk-react": "file:../react", "@clerk/elements": "file:../elements", "@clerk/shared": "file:../shared", "@clerk/types": "file:../types", diff --git a/packages/ui/package.json b/packages/ui/package.json index 6f009787b5d..72aa9f2978d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -29,13 +29,13 @@ "./*": { "import": { "types": "./dist/components/*.d.mts", - "default": "./dist/components/*.mjs", - "browser": "./dist/components/*.mjs" + "browser": "./dist/components/*.mjs", + "default": "./dist/components/*.mjs" }, "require": { "types": "./dist/components/*.d.ts", - "default": "./dist/components/*.js", - "browser": "./dist/components/*.js" + "browser": "./dist/components/*.js", + "default": "./dist/components/*.js" } }, "./styles.css": "./dist/styles.css" diff --git a/packages/ui/src/common/password-field.tsx b/packages/ui/src/common/password-field.tsx index 821b5af0f37..6bf11144575 100644 --- a/packages/ui/src/common/password-field.tsx +++ b/packages/ui/src/common/password-field.tsx @@ -9,21 +9,28 @@ import EyeSlashSm from '~/primitives/icons/eye-slash-sm'; import EyeSm from '~/primitives/icons/eye-sm'; import { translatePasswordError } from '~/utils/make-localizable'; -export function PasswordField({ - alternativeFieldTrigger, - className, - label, - name = 'password', - ...props -}: { - alternativeFieldTrigger?: React.ReactNode; - validatePassword?: boolean; - name?: 'password' | 'confirmPassword'; - /** - * **Note:** this prop is required as the `label` differs depending on the context (e.g. new password) - */ - label: React.ReactNode; -} & Omit, 'autoCapitalize' | 'autoComplete' | 'spellCheck' | 'type'>) { +export const PasswordField = React.forwardRef(function PasswordField( + { + alternativeFieldTrigger, + className, + fieldClassName, + fieldRef, + label, + name = 'password', + ...props + }: { + alternativeFieldTrigger?: React.ReactNode; + validatePassword?: boolean; + name?: 'password' | 'confirmPassword'; + /** + * **Note:** this prop is required as the `label` differs depending on the context (e.g. new password) + */ + label: React.ReactNode; + fieldRef?: React.Ref; + fieldClassName?: string; + } & Omit, 'autoCapitalize' | 'autoComplete' | 'spellCheck' | 'type'>, + forwardedRef: React.ForwardedRef, +) { const [type, setType] = React.useState('password'); const id = React.useId(); const { t, locale } = useLocalizations(); @@ -33,7 +40,10 @@ export function PasswordField({ name={name} asChild > - + {label} @@ -48,6 +58,7 @@ export function PasswordField({ type={type} className={cx('pe-7', className)} {...props} + ref={forwardedRef} aria-describedby={props.validatePassword && state !== 'idle' ? id : undefined} asChild > @@ -121,4 +132,4 @@ export function PasswordField({ ); -} +}); diff --git a/packages/ui/src/common/router-link.tsx b/packages/ui/src/common/router-link.tsx new file mode 100644 index 00000000000..944de7df288 --- /dev/null +++ b/packages/ui/src/common/router-link.tsx @@ -0,0 +1,28 @@ +import { useClerkHostRouter } from '@clerk/shared/router'; +import { Slot } from '@radix-ui/react-slot'; +import * as React from 'react'; + +export const RouterLink = React.forwardRef< + HTMLAnchorElement, + React.AnchorHTMLAttributes & { + asChild?: boolean; + } +>(function RouterLink({ asChild, children, href, ...props }, forwardedRef) { + const router = useClerkHostRouter(); + const Comp = asChild ? Slot : 'a'; + return ( + { + e.preventDefault(); + if (href) { + router.push(href); + } + }} + > + {children} + + ); +}); diff --git a/packages/ui/src/components/sign-in/steps/start.tsx b/packages/ui/src/components/sign-in/steps/start.tsx index a37958b4b45..f63b9b52d02 100644 --- a/packages/ui/src/components/sign-in/steps/start.tsx +++ b/packages/ui/src/components/sign-in/steps/start.tsx @@ -1,5 +1,8 @@ import * as Common from '@clerk/elements/common'; import * as SignIn from '@clerk/elements/sign-in'; +import { useClerk } from '@clerk/shared/react'; +import { cx } from 'cva'; +import * as React from 'react'; import { Connections } from '~/common/connections'; import { EmailField } from '~/common/email-field'; @@ -7,8 +10,10 @@ import { EmailOrPhoneNumberField } from '~/common/email-or-phone-number-field'; import { EmailOrUsernameField } from '~/common/email-or-username-field'; import { EmailOrUsernameOrPhoneNumberField } from '~/common/email-or-username-or-phone-number-field'; import { GlobalError } from '~/common/global-error'; +import { PasswordField } from '~/common/password-field'; import { PhoneNumberField } from '~/common/phone-number-field'; import { PhoneNumberOrUsernameField } from '~/common/phone-number-or-username-field'; +import { RouterLink } from '~/common/router-link'; import { UsernameField } from '~/common/username-field'; import { LOCALIZATION_NEEDED } from '~/constants/localizations'; import { SIGN_UP_MODES } from '~/constants/user-settings'; @@ -20,6 +25,7 @@ import { useDisplayConfig } from '~/hooks/use-display-config'; import { useEnabledConnections } from '~/hooks/use-enabled-connections'; import { useEnvironment } from '~/hooks/use-environment'; import { useLocalizations } from '~/hooks/use-localizations'; +import { useOptions } from '~/hooks/use-options'; import { Button } from '~/primitives/button'; import * as Card from '~/primitives/card'; import CaretRightLegacySm from '~/primitives/icons/caret-right-legacy-sm'; @@ -41,6 +47,8 @@ export function SignInStart() { const isDev = useDevModeWarning(); const { options } = useAppearance().parsedAppearance; const { logoProps, footerProps } = useCard(); + const clerk = useClerk(); + const { signUpUrl } = useOptions(); return ( @@ -136,6 +144,8 @@ export function SignInStart() { required /> ) : null} + + ) : null} {options.socialButtonsPlacement === 'bottom' ? connectionsWithSeperator.reverse() : null} @@ -191,7 +201,12 @@ export function SignInStart() { {t('signIn.start.actionText')}{' '} - {t('signIn.start.actionLink')} + + {t('signIn.start.actionLink')} + ) : null} @@ -203,3 +218,36 @@ export function SignInStart() { ); } + +function AutoFillPasswordField() { + const { t } = useLocalizations(); + const [isAutoFilled, setIsAutoFilled] = React.useState(false); + const fieldRef = React.useRef(null); + + const handleAutofill = (event: React.ChangeEvent) => { + if (event.target.value && !isAutoFilled) { + setIsAutoFilled(true); + } + }; + + React.useEffect(() => { + if (fieldRef.current) { + fieldRef.current.setAttribute('inert', ''); + } + }, []); + + React.useEffect(() => { + if (fieldRef.current && isAutoFilled) { + fieldRef.current.removeAttribute('inert'); + } + }, [isAutoFilled]); + + return ( + + ); +} diff --git a/packages/ui/src/components/sign-up/steps/continue.tsx b/packages/ui/src/components/sign-up/steps/continue.tsx index 600d9e82a99..3690611737f 100644 --- a/packages/ui/src/components/sign-up/steps/continue.tsx +++ b/packages/ui/src/components/sign-up/steps/continue.tsx @@ -1,5 +1,6 @@ import * as Common from '@clerk/elements/common'; import * as SignUp from '@clerk/elements/sign-up'; +import { useClerk } from '@clerk/shared/react'; import { EmailField } from '~/common/email-field'; import { FirstNameField } from '~/common/first-name-field'; @@ -7,17 +8,21 @@ import { GlobalError } from '~/common/global-error'; import { LastNameField } from '~/common/last-name-field'; import { PasswordField } from '~/common/password-field'; import { PhoneNumberField } from '~/common/phone-number-field'; +import { RouterLink } from '~/common/router-link'; import { UsernameField } from '~/common/username-field'; import { LOCALIZATION_NEEDED } from '~/constants/localizations'; import { useAttributes } from '~/hooks/use-attributes'; import { useCard } from '~/hooks/use-card'; import { useDevModeWarning } from '~/hooks/use-dev-mode-warning'; import { useLocalizations } from '~/hooks/use-localizations'; +import { useOptions } from '~/hooks/use-options'; import { Button } from '~/primitives/button'; import * as Card from '~/primitives/card'; import CaretRightLegacySm from '~/primitives/icons/caret-right-legacy-sm'; export function SignUpContinue() { + const clerk = useClerk(); + const { signInUrl } = useOptions(); const { t } = useLocalizations(); const { enabled: firstNameEnabled, required: firstNameRequired } = useAttributes('first_name'); const { enabled: lastNameEnabled, required: lastNameRequired } = useAttributes('last_name'); @@ -114,7 +119,12 @@ export function SignUpContinue() { {t('signUp.continue.actionText')}{' '} - {t('signUp.continue.actionLink')} + + {t('signUp.continue.actionLink')} + diff --git a/packages/ui/src/components/sign-up/steps/start.tsx b/packages/ui/src/components/sign-up/steps/start.tsx index 1325ea807c4..90450609b66 100644 --- a/packages/ui/src/components/sign-up/steps/start.tsx +++ b/packages/ui/src/components/sign-up/steps/start.tsx @@ -10,6 +10,7 @@ import { GlobalError } from '~/common/global-error'; import { LastNameField } from '~/common/last-name-field'; import { PasswordField } from '~/common/password-field'; import { PhoneNumberField } from '~/common/phone-number-field'; +import { RouterLink } from '~/common/router-link'; import { UsernameField } from '~/common/username-field'; import { LOCALIZATION_NEEDED } from '~/constants/localizations'; import { useAppearance } from '~/contexts'; @@ -20,6 +21,7 @@ import { useDisplayConfig } from '~/hooks/use-display-config'; import { useEnabledConnections } from '~/hooks/use-enabled-connections'; import { useEnvironment } from '~/hooks/use-environment'; import { useLocalizations } from '~/hooks/use-localizations'; +import { useOptions } from '~/hooks/use-options'; import { Button } from '~/primitives/button'; import * as Card from '~/primitives/card'; import CaretRightLegacySm from '~/primitives/icons/caret-right-legacy-sm'; @@ -27,6 +29,7 @@ import { Separator } from '~/primitives/separator'; export function SignUpStart() { const clerk = useClerk(); + const { signInUrl } = useOptions(); const enabledConnections = useEnabledConnections(); const { userSettings } = useEnvironment(); const { t } = useLocalizations(); @@ -170,7 +173,12 @@ export function SignUpStart() { {t('signUp.start.actionText')}{' '} - {t('signUp.start.actionLink')} + + {t('signUp.start.actionLink')} + From 2cc9acd40b90d03895f8664740c9f9b9892e2c0f Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 4 Oct 2024 13:46:29 +0300 Subject: [PATCH 28/37] chore(elements): Align clerk dependencies (#4282) --- .changeset/late-kiwis-warn.md | 5 +++++ package-lock.json | 6 ++---- packages/elements/package.json | 6 ++---- 3 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 .changeset/late-kiwis-warn.md diff --git a/.changeset/late-kiwis-warn.md b/.changeset/late-kiwis-warn.md new file mode 100644 index 00000000000..6ab9fa5a6be --- /dev/null +++ b/.changeset/late-kiwis-warn.md @@ -0,0 +1,5 @@ +--- +"@clerk/elements": patch +--- + +Remove @clerk/clerk-react as a dev depedency. Move @clerk/shared to depedencies (previously devDepedencies). diff --git a/package-lock.json b/package-lock.json index e341029a900..9736770e5eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45783,7 +45783,8 @@ "version": "0.15.9", "license": "MIT", "dependencies": { - "@clerk/types": "^4.25.0", + "@clerk/shared": "2.9.0", + "@clerk/types": "4.25.0", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-slot": "^1.1.0", "@xstate/react": "^4.1.1", @@ -45791,9 +45792,7 @@ "xstate": "^5.15.0" }, "devDependencies": { - "@clerk/clerk-react": "5.11.0", "@clerk/eslint-config-custom": "*", - "@clerk/shared": "2.9.0", "@statelyai/inspect": "^0.4.0", "@types/node": "^18.19.33", "@types/react": "*", @@ -45809,7 +45808,6 @@ "node": ">=18.17.0" }, "peerDependencies": { - "@clerk/shared": "^2.0.0", "react": "^18.0.0 || ^19.0.0-beta", "react-dom": "^18.0.0 || ^19.0.0-beta" }, diff --git a/packages/elements/package.json b/packages/elements/package.json index 70c07d70848..fd3faf0950c 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -71,7 +71,8 @@ "test:cache:clear": "jest --clearCache --useStderr" }, "dependencies": { - "@clerk/types": "^4.25.0", + "@clerk/shared": "2.9.0", + "@clerk/types": "4.25.0", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-slot": "^1.1.0", "@xstate/react": "^4.1.1", @@ -79,9 +80,7 @@ "xstate": "^5.15.0" }, "devDependencies": { - "@clerk/clerk-react": "5.11.0", "@clerk/eslint-config-custom": "*", - "@clerk/shared": "2.9.0", "@statelyai/inspect": "^0.4.0", "@types/node": "^18.19.33", "@types/react": "*", @@ -94,7 +93,6 @@ "typescript": "*" }, "peerDependencies": { - "@clerk/shared": "^2.0.0", "react": "^18.0.0 || ^19.0.0-beta", "react-dom": "^18.0.0 || ^19.0.0-beta" }, From 77bd3e090f1b92535a73d521d26f15bfe7a5adea Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:11:23 -0700 Subject: [PATCH 29/37] feat(types,ui): Add support for `__experimental.appearance` prop (#4290) --- .changeset/unlucky-steaks-protect.md | 5 +++ packages/types/src/clerk.ts | 14 ++------ .../ui/src/components/sign-in/sign-in.tsx | 9 +++-- .../ui/src/components/sign-up/sign-up.tsx | 5 ++- .../ui/src/contexts/AppearanceContext.tsx | 5 +-- packages/ui/src/primitives/card.tsx | 34 ++++++++++++++++--- packages/ui/src/themes/full.ts | 3 +- packages/ui/src/themes/layout.ts | 3 +- 8 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 .changeset/unlucky-steaks-protect.md diff --git a/.changeset/unlucky-steaks-protect.md b/.changeset/unlucky-steaks-protect.md new file mode 100644 index 00000000000..e5a01da6c27 --- /dev/null +++ b/.changeset/unlucky-steaks-protect.md @@ -0,0 +1,5 @@ +--- +"@clerk/types": patch +--- + +Fix `SignInProps`/`SignUpProps` `__experimental` type to allow for arbitrary properties diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 3a452df4e12..877a34c4e1e 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -837,12 +837,7 @@ export type SignInProps = RoutingOptions & { /** * Enable experimental flags to gain access to new features. These flags are not guaranteed to be stable and may change drastically in between patch or minor versions. */ - __experimental?: Autocomplete< - { - newComponents: boolean; - }, - Record - >; + __experimental?: Record & { newComponents?: boolean }; } & TransferableOption & SignUpForceRedirectUrl & SignUpFallbackRedirectUrl & @@ -952,12 +947,7 @@ export type SignUpProps = RoutingOptions & { /** * Enable experimental flags to gain access to new features. These flags are not guaranteed to be stable and may change drastically in between patch or minor versions. */ - __experimental?: Autocomplete< - { - newComponents: boolean; - }, - Record - >; + __experimental?: Record & { newComponents?: boolean }; } & SignInFallbackRedirectUrl & SignInForceRedirectUrl & LegacyRedirectProps & diff --git a/packages/ui/src/components/sign-in/sign-in.tsx b/packages/ui/src/components/sign-in/sign-in.tsx index 9a5ac9a0714..28ca14f01f9 100644 --- a/packages/ui/src/components/sign-in/sign-in.tsx +++ b/packages/ui/src/components/sign-in/sign-in.tsx @@ -24,13 +24,16 @@ import { type Appearance, AppearanceProvider } from '~/contexts'; * where we'll consider its integration within Elements, as well as ensure * bulletproof a11y. */ -export function SignIn({ appearance, ...rest }: { appearance?: Appearance } & SignInProps) { +export function SignIn({ appearance, ...props }: { appearance?: Appearance } & SignInProps) { const [showHelp, setShowHelp] = React.useState(false); + // If __experimental.newComponents is `true`, we should use __experimental.appearance instead of appearance. + const componentAppearance = props.__experimental?.newComponents ? props.__experimental.appearance : appearance; + return ( - + - + {showHelp ? ( ) : ( diff --git a/packages/ui/src/components/sign-up/sign-up.tsx b/packages/ui/src/components/sign-up/sign-up.tsx index 15d59b39f2f..835bb2d231b 100644 --- a/packages/ui/src/components/sign-up/sign-up.tsx +++ b/packages/ui/src/components/sign-up/sign-up.tsx @@ -8,8 +8,11 @@ import { SignUpVerifications } from '~/components/sign-up/steps/verifications'; import { type Appearance, AppearanceProvider } from '~/contexts'; export function SignUp({ appearance, ...props }: { appearance?: Appearance } & SignUpProps) { + // If __experimental.newComponents is `true`, we should use __experimental.appearance instead of appearance. + const componentAppearance = props.__experimental?.newComponents ? props.__experimental.appearance : appearance; + return ( - + diff --git a/packages/ui/src/contexts/AppearanceContext.tsx b/packages/ui/src/contexts/AppearanceContext.tsx index 90c1882173e..51b8fcdab42 100644 --- a/packages/ui/src/contexts/AppearanceContext.tsx +++ b/packages/ui/src/contexts/AppearanceContext.tsx @@ -4,13 +4,14 @@ import React from 'react'; import { fullTheme } from '~/themes'; -type AlertDescriptorIdentifier = 'alert' | 'alert__error' | 'alert__warning' | 'alertRoot' | 'alertIcon'; +type AlertDescriptorIdentifier = 'alert' | 'alert__error' | 'alert__warning' | 'alertIcon'; type SeparatorDescriptorIdentifier = 'separator'; +type CardDescriptorIdentifier = 'logoBox' | 'logoLink' | 'logoImage'; /** * Union of all valid descriptors used throughout the components. */ -export type DescriptorIdentifier = AlertDescriptorIdentifier | SeparatorDescriptorIdentifier; +export type DescriptorIdentifier = AlertDescriptorIdentifier | SeparatorDescriptorIdentifier | CardDescriptorIdentifier; /** * The final resulting descriptor that gets passed to mergeDescriptors and spread on the element. diff --git a/packages/ui/src/primitives/card.tsx b/packages/ui/src/primitives/card.tsx index 0df9a6f4c4e..3e0ee817d08 100644 --- a/packages/ui/src/primitives/card.tsx +++ b/packages/ui/src/primitives/card.tsx @@ -1,6 +1,8 @@ import { cva, cx } from 'cva'; import * as React from 'react'; +import { useAppearance } from '~/contexts'; +import { mergeDescriptors, type ParsedElementsFragment } from '~/contexts/AppearanceContext'; import type { PolymorphicForwardRefExoticComponent, PolymorphicPropsWithoutRef } from '~/types/utils'; import { ClerkLogo } from './clerk-logo'; @@ -106,9 +108,24 @@ export const Header = React.forwardRef, ) { + const { elements } = useAppearance().parsedAppearance; if (!src) { return null; } @@ -128,15 +146,15 @@ export const Logo = React.forwardRef(function CardLogo( src={src} size={200} {...props} - className={cx('size-full object-contain', className)} + {...mergeDescriptors(elements.logoImage)} /> ); return ( -
+
{href ? ( {img} @@ -384,3 +402,11 @@ const FooterPageLink = React.forwardRef Date: Fri, 4 Oct 2024 13:20:02 -0700 Subject: [PATCH 30/37] fix(nextjs): Do not export useNextRouter --- packages/nextjs/src/app-router/client/ClerkProvider.tsx | 4 ++-- packages/nextjs/src/pages/ClerkProvider.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index fa69ff49271..af71badee47 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -27,12 +27,12 @@ declare global { // The version that Next added support for the window.history.pushState and replaceState APIs. // ref: https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate -export const NEXT_WINDOW_HISTORY_SUPPORT_VERSION = '14.1.0'; +const NEXT_WINDOW_HISTORY_SUPPORT_VERSION = '14.1.0'; /** * Clerk router integration with Next.js's router. */ -export const useNextRouter = (): ClerkHostRouter => { +const useNextRouter = (): ClerkHostRouter => { const router = useRouter(); // The window.history APIs seem to prevent Next.js from triggering a full page re-render, allowing us to diff --git a/packages/nextjs/src/pages/ClerkProvider.tsx b/packages/nextjs/src/pages/ClerkProvider.tsx index 00253fd7630..1da9a7c7259 100644 --- a/packages/nextjs/src/pages/ClerkProvider.tsx +++ b/packages/nextjs/src/pages/ClerkProvider.tsx @@ -18,12 +18,12 @@ setClerkJsLoadingErrorPackageName(PACKAGE_NAME); // The version that Next added support for the window.history.pushState and replaceState APIs. // ref: https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate -export const NEXT_WINDOW_HISTORY_SUPPORT_VERSION = '14.1.0'; +const NEXT_WINDOW_HISTORY_SUPPORT_VERSION = '14.1.0'; /** * Clerk router integration with Next.js's router. */ -export const useNextRouter = (): ClerkHostRouter => { +const useNextRouter = (): ClerkHostRouter => { const router = useRouter(); // The window.history APIs seem to prevent Next.js from triggering a full page re-render, allowing us to From 74c81bf8c15e9669ec175a515fa3b5ce610698f4 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:53:36 -0700 Subject: [PATCH 31/37] feat(clerk-js): Add support for loading UI styles as first CSS stylesheet (#4291) --- packages/clerk-js/global.d.ts | 4 ---- packages/clerk-js/src/global.d.ts | 4 ++++ packages/clerk-js/src/ui/new/renderer.tsx | 16 +++++++++++++--- packages/clerk-js/webpack.config.js | 16 +++++++++++----- 4 files changed, 28 insertions(+), 12 deletions(-) delete mode 100644 packages/clerk-js/global.d.ts create mode 100644 packages/clerk-js/src/global.d.ts diff --git a/packages/clerk-js/global.d.ts b/packages/clerk-js/global.d.ts deleted file mode 100644 index 4684851c62a..00000000000 --- a/packages/clerk-js/global.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.module.scss' { - const content: Record; - export default content; -} diff --git a/packages/clerk-js/src/global.d.ts b/packages/clerk-js/src/global.d.ts new file mode 100644 index 00000000000..4882a05c185 --- /dev/null +++ b/packages/clerk-js/src/global.d.ts @@ -0,0 +1,4 @@ +declare module '@clerk/ui/styles.css' { + const content: string; + export default content; +} diff --git a/packages/clerk-js/src/ui/new/renderer.tsx b/packages/clerk-js/src/ui/new/renderer.tsx index a90a5dc7504..4c6623c3f0b 100644 --- a/packages/clerk-js/src/ui/new/renderer.tsx +++ b/packages/clerk-js/src/ui/new/renderer.tsx @@ -1,10 +1,8 @@ -// TODO: don't import here -import '@clerk/ui/styles.css'; - import { ClerkInstanceContext, OptionsContext } from '@clerk/shared/react'; import type { ClerkHostRouter } from '@clerk/shared/router'; import { ClerkHostRouterContext } from '@clerk/shared/router'; import type { ClerkOptions, LoadedClerk } from '@clerk/types'; +import stylesheetURL from '@clerk/ui/styles.css'; import type { ElementType, ReactNode } from 'react'; import { createElement, lazy } from 'react'; import { createPortal } from 'react-dom'; @@ -43,6 +41,18 @@ export function init({ wrapper }: { wrapper: ElementType }) { rootElement = document.createElement('div'); rootElement.setAttribute('id', 'clerk-components'); document.body.appendChild(rootElement); + + // Just for completeness, we check to see if we've already added the stylesheet to the DOM. + const STYLESHEET_SIGIL = 'data-clerk-styles'; + const existingStylesheet = document.querySelector(`link[${STYLESHEET_SIGIL}]`); + if (!existingStylesheet) { + const stylesheet = document.createElement('link'); + stylesheet.href = stylesheetURL; + stylesheet.rel = 'stylesheet'; + stylesheet.setAttribute(STYLESHEET_SIGIL, ''); + // Add as first stylesheet so that application styles take precedence over our styles. + document.head.prepend(stylesheet); + } } const root = createRoot(rootElement); diff --git a/packages/clerk-js/webpack.config.js b/packages/clerk-js/webpack.config.js index 235bad38b48..f3ba0d186e8 100644 --- a/packages/clerk-js/webpack.config.js +++ b/packages/clerk-js/webpack.config.js @@ -106,9 +106,6 @@ const common = ({ mode }) => { }, }, }, - experiments: { - css: true, - }, }; }; @@ -173,12 +170,21 @@ const typescriptLoaderDev = () => { }; }; +/** @type { () => (import('webpack').RuleSetRule) } */ +const clerkUICSSLoader = () => { + // This emits a module exporting the URL to the styles.css file. + return { + test: /packages\/ui\/dist\/styles\.css/, + type: 'asset/resource', + }; +}; + /** @type { () => (import('webpack').Configuration) } */ const commonForProd = () => { return { devtool: undefined, module: { - rules: [svgLoader(), typescriptLoaderProd()], + rules: [svgLoader(), typescriptLoaderProd(), clerkUICSSLoader()], }, output: { path: path.resolve(__dirname, 'dist'), @@ -297,7 +303,7 @@ const devConfig = ({ mode, env }) => { const commonForDev = () => { return { module: { - rules: [svgLoader(), typescriptLoaderDev()], + rules: [svgLoader(), typescriptLoaderDev(), clerkUICSSLoader()], }, plugins: [ new ReactRefreshWebpackPlugin({ overlay: { sockHost: devUrl.host } }), From 4746fb402d61c37873875d21b96e9f0459f951b5 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:10:03 -0700 Subject: [PATCH 32/37] fix(clerk-js): Ensure import.meta.url isn't used in CJS build --- packages/clerk-js/webpack.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/webpack.config.js b/packages/clerk-js/webpack.config.js index f3ba0d186e8..f1926ed67c0 100644 --- a/packages/clerk-js/webpack.config.js +++ b/packages/clerk-js/webpack.config.js @@ -193,7 +193,7 @@ const commonForProd = () => { globalObject: 'globalThis', }, optimization: { - minimize: true, + minimize: false, minimizer: [ compiler => { new TerserPlugin({ @@ -288,6 +288,7 @@ const prodConfig = ({ mode }) => { filename: '[name].js', libraryTarget: 'commonjs', chunkFormat: 'commonjs', + scriptType: 'text/javascript', }, }); From 54a836cc80a0cd6eda1e5741a025a7f14801164a Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:21:31 -0700 Subject: [PATCH 33/37] fix(clerk-js): Restore minimization --- packages/clerk-js/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/webpack.config.js b/packages/clerk-js/webpack.config.js index f1926ed67c0..92960719330 100644 --- a/packages/clerk-js/webpack.config.js +++ b/packages/clerk-js/webpack.config.js @@ -193,7 +193,7 @@ const commonForProd = () => { globalObject: 'globalThis', }, optimization: { - minimize: false, + minimize: true, minimizer: [ compiler => { new TerserPlugin({ From 29258d0cbe2beb64ebebef38b98dbbb0ad0d8ecb Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:35:42 -0700 Subject: [PATCH 34/37] fix(clerk-js): Bundle CSS into CJS and ESM bundles (#4301) --- packages/clerk-js/src/ui/new/renderer.tsx | 22 ++++++-- packages/clerk-js/webpack.config.js | 69 +++++++++++++++++++---- 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/packages/clerk-js/src/ui/new/renderer.tsx b/packages/clerk-js/src/ui/new/renderer.tsx index 4c6623c3f0b..95358f25f39 100644 --- a/packages/clerk-js/src/ui/new/renderer.tsx +++ b/packages/clerk-js/src/ui/new/renderer.tsx @@ -2,7 +2,7 @@ import { ClerkInstanceContext, OptionsContext } from '@clerk/shared/react'; import type { ClerkHostRouter } from '@clerk/shared/router'; import { ClerkHostRouterContext } from '@clerk/shared/router'; import type { ClerkOptions, LoadedClerk } from '@clerk/types'; -import stylesheetURL from '@clerk/ui/styles.css'; +import stylesheetURLOrContent from '@clerk/ui/styles.css'; import type { ElementType, ReactNode } from 'react'; import { createElement, lazy } from 'react'; import { createPortal } from 'react-dom'; @@ -43,12 +43,22 @@ export function init({ wrapper }: { wrapper: ElementType }) { document.body.appendChild(rootElement); // Just for completeness, we check to see if we've already added the stylesheet to the DOM. - const STYLESHEET_SIGIL = 'data-clerk-styles'; - const existingStylesheet = document.querySelector(`link[${STYLESHEET_SIGIL}]`); + const STYLESHEET_SIGIL = 'data-clerk-injected-styles'; + const existingStylesheet = document.querySelector(`[${STYLESHEET_SIGIL}]`); if (!existingStylesheet) { - const stylesheet = document.createElement('link'); - stylesheet.href = stylesheetURL; - stylesheet.rel = 'stylesheet'; + let stylesheet: HTMLLinkElement | HTMLStyleElement; + + if (stylesheetURLOrContent.endsWith('.css')) { + // stylesheetURLOrContent is a URL to a stylesheet + stylesheet = document.createElement('link'); + (stylesheet as HTMLLinkElement).href = stylesheetURLOrContent; + (stylesheet as HTMLLinkElement).rel = 'stylesheet'; + } else { + // stylesheetURLOrContent is CSS + stylesheet = document.createElement('style'); + stylesheet.textContent = stylesheetURLOrContent; + } + stylesheet.setAttribute(STYLESHEET_SIGIL, ''); // Add as first stylesheet so that application styles take precedence over our styles. document.head.prepend(stylesheet); diff --git a/packages/clerk-js/webpack.config.js b/packages/clerk-js/webpack.config.js index 92960719330..c74ce2e7502 100644 --- a/packages/clerk-js/webpack.config.js +++ b/packages/clerk-js/webpack.config.js @@ -170,22 +170,58 @@ const typescriptLoaderDev = () => { }; }; -/** @type { () => (import('webpack').RuleSetRule) } */ +/** + * Used in outputs that utilize chunking, and returns a URL to the stylesheet. + * @type { () => (import('webpack').RuleSetRule) } + */ const clerkUICSSLoader = () => { - // This emits a module exporting the URL to the styles.css file. + // This emits a module exporting a URL to the styles.css file. return { test: /packages\/ui\/dist\/styles\.css/, type: 'asset/resource', }; }; -/** @type { () => (import('webpack').Configuration) } */ -const commonForProd = () => { +/** + * Used in outputs that _do not_ utilize chunking, and returns the contents of the stylesheet. + * @type { () => (import('webpack').RuleSetRule) } + */ +const clerkUICSSSourceLoader = () => { + // This emits a module exporting the contents of the styles.css file. + return { + test: /packages\/ui\/dist\/styles\.css/, + type: 'asset/source', + }; +}; + +/** + * Used for production builds that have dynamicly loaded chunks. + * @type { () => (import('webpack').Configuration) } + * */ +const commonForProdChunked = () => { return { - devtool: undefined, module: { rules: [svgLoader(), typescriptLoaderProd(), clerkUICSSLoader()], }, + }; +}; + +/** + * Used for production builds that combine all files into one single file (such as for Chrome Extensions). + * @type { () => (import('webpack').Configuration) } + * */ +const commonForProdBundled = () => { + return { + module: { + rules: [svgLoader(), typescriptLoaderProd(), clerkUICSSSourceLoader()], + }, + }; +}; + +/** @type { () => (import('webpack').Configuration) } */ +const commonForProd = () => { + return { + devtool: undefined, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js', @@ -238,12 +274,18 @@ const entryForVariant = variant => { /** @type { () => (import('webpack').Configuration)[] } */ const prodConfig = ({ mode }) => { - const clerkBrowser = merge(entryForVariant(variants.clerkBrowser), common({ mode }), commonForProd()); + const clerkBrowser = merge( + entryForVariant(variants.clerkBrowser), + common({ mode }), + commonForProd(), + commonForProdChunked(), + ); const clerkHeadless = merge( entryForVariant(variants.clerkHeadless), common({ mode }), commonForProd(), + commonForProdChunked(), // Disable chunking for the headless variant, since it's meant to be used in a non-browser environment and // attempting to load chunks causes issues due to usage of a dynamic publicPath. We generally are only concerned with // chunking in our browser bundles. @@ -262,10 +304,11 @@ const prodConfig = ({ mode }) => { entryForVariant(variants.clerkHeadlessBrowser), common({ mode }), commonForProd(), + commonForProdChunked(), // externalsForHeadless(), ); - const clerkEsm = merge(entryForVariant(variants.clerk), common({ mode }), commonForProd(), { + const clerkEsm = merge(entryForVariant(variants.clerk), common({ mode }), commonForProd(), commonForProdBundled(), { experiments: { outputModule: true, }, @@ -283,13 +326,19 @@ const prodConfig = ({ mode }) => { ], }); - const clerkCjs = merge(clerkEsm, { + const clerkCjs = merge(entryForVariant(variants.clerk), common({ mode }), commonForProd(), commonForProdBundled(), { output: { filename: '[name].js', libraryTarget: 'commonjs', - chunkFormat: 'commonjs', - scriptType: 'text/javascript', }, + plugins: [ + // Include the lazy chunks in the bundle as well + // so that the final bundle can be imported and bundled again + // by a different bundler, eg the webpack instance used by react-scripts + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], }); return [clerkBrowser, clerkHeadless, clerkHeadlessBrowser, clerkEsm, clerkCjs]; From 62dbb34d11961714fc3ff0bb33e5a3606e282ef3 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:48:10 -0700 Subject: [PATCH 35/37] fix(ui): Fix package.json --- packages/ui/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index ba213a74c11..fab9ec5c559 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -12,7 +12,6 @@ "email": "support@clerk.com", "url": "git+https://github.com/clerk/javascript.git" }, - }, "sideEffects": [ "*.css" ], From 163ef011f2086640777b86aa58bb9fe5bbd80e4e Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:48:36 -0700 Subject: [PATCH 36/37] fix(elements): Move shared back to dependencies --- packages/elements/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/elements/package.json b/packages/elements/package.json index 83738bea831..66b2d8288d3 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -71,6 +71,7 @@ "test:cache:clear": "jest --clearCache --useStderr" }, "dependencies": { + "@clerk/shared": "2.9.1", "@clerk/types": "^4.25.1", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-slot": "^1.1.0", @@ -81,7 +82,6 @@ "devDependencies": { "@clerk/clerk-react": "5.11.1", "@clerk/eslint-config-custom": "*", - "@clerk/shared": "2.9.1", "@statelyai/inspect": "^0.4.0", "@types/node": "^18.19.33", "@types/react": "*", From 157401437d080335740550446395963c971b931f Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Thu, 17 Oct 2024 10:38:49 -0700 Subject: [PATCH 37/37] fix(elements: Use latest version of shared --- package-lock.json | 36 ++-------------------------------- packages/elements/package.json | 2 +- 2 files changed, 3 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9aeda60d89..cb1aac77c73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44865,7 +44865,7 @@ "version": "0.16.2", "license": "MIT", "dependencies": { - "@clerk/shared": "2.9.1", + "@clerk/shared": "2.9.2", "@clerk/types": "^4.26.0", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-slot": "^1.1.0", @@ -44900,38 +44900,11 @@ } } }, - "packages/elements/node_modules/@clerk/shared": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-2.9.1.tgz", - "integrity": "sha512-NOO6gLRKvEMmz/3pjDiG/rE7ZSmLbZFaU1yTyx94Wb33v0sAaF4v8uypbXSswgAsPWVKu+fgkWmmbhBJP5Us7A==", - "hasInstallScript": true, - "dependencies": { - "@clerk/types": "4.25.1", - "glob-to-regexp": "0.4.1", - "js-cookie": "3.0.5", - "std-env": "^3.7.0", - "swr": "^2.2.0" - }, - "engines": { - "node": ">=18.17.0" - }, - "peerDependencies": { - "react": ">=18 || >=19.0.0-beta", - "react-dom": ">=18 || >=19.0.0-beta" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "packages/elements/node_modules/@clerk/shared/node_modules/@clerk/types": { "version": "4.25.1", "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.25.1.tgz", "integrity": "sha512-ILvR2YXz6BSGXDoozBAd2BGj8ZF/FQrfWQd0FtLz1JXt1vurkgRncAhEcC427OiXNRnq5R2Pn++urfFEa0PqYA==", + "extraneous": true, "dependencies": { "csstype": "3.1.1" }, @@ -45263,11 +45236,6 @@ } } }, - "packages/elements/node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, "packages/elements/node_modules/next": { "version": "14.2.4", "dev": true, diff --git a/packages/elements/package.json b/packages/elements/package.json index 7954591b24d..e46d3c6f468 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -71,7 +71,7 @@ "test:cache:clear": "jest --clearCache --useStderr" }, "dependencies": { - "@clerk/shared": "2.9.1", + "@clerk/shared": "2.9.2", "@clerk/types": "^4.26.0", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-slot": "^1.1.0",