-
Notifications
You must be signed in to change notification settings - Fork 244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(clerk-js): Render @clerk/ui
#4114
base: main
Are you sure you want to change the base?
Changes from all commits
9e71651
8f3ea4b
b3ee721
851f201
371f6ee
b8d014f
56af4f5
f0c6f51
2fdd33e
5322329
b9b3359
e3132e3
f000689
9866b16
812035a
da7ba53
3fe0bda
9542bab
a75375f
7f71197
31d749c
4b6c093
86419e0
93c17d2
e264672
4e84b25
4b50d86
02ac475
c245d6f
2aab6cd
8ab9b83
368db81
b40c293
fc3f448
d415f19
477ee05
860897c
2cc9acd
07a26ab
77bd3e0
6bae6c3
3d6d9ae
74c81bf
63f97d3
4746fb4
54a836c
29258d0
c65ea96
bab0ad3
129bc20
98fb7db
62dbb34
dc4149e
163ef01
5035d98
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@clerk/elements": patch | ||
--- | ||
|
||
Remove @clerk/clerk-react as a dev depedency. Move @clerk/shared to depedencies (previously devDepedencies). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@clerk/types": patch | ||
--- | ||
|
||
Fix `SignInProps`/`SignUpProps` `__experimental` type to allow for arbitrary properties |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,6 +53,7 @@ | |
"@clerk/localizations": "3.2.1", | ||
"@clerk/shared": "2.9.1", | ||
"@clerk/types": "4.25.1", | ||
"@clerk/ui": "0.1.9", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [question] is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, it's an internal-only package essentially. At this point I'm not aware of any plans to make it a publicly consumable package, so we might be able to get away with keeping it private and pinning the version to The |
||
"@coinbase/wallet-sdk": "4.0.4", | ||
"@emotion/cache": "11.11.0", | ||
"@emotion/react": "11.11.1", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
declare module '@clerk/ui/styles.css' { | ||
const content: string; | ||
export default content; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { createDeferredPromise } from '@clerk/shared'; | ||
import type { ClerkHostRouter } from '@clerk/shared/router'; | ||
import type { ClerkOptions, LoadedClerk } from '@clerk/types'; | ||
|
||
import type { init } from './renderer'; | ||
import type { ClerkNewComponents, ComponentDefinition } from './types'; | ||
|
||
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; | ||
clerk: LoadedClerk; | ||
options: ClerkOptions; | ||
componentRegistry = new Map<string, ComponentDefinition>(); | ||
|
||
#rendererPromise?: ReturnType<typeof createDeferredPromise>; | ||
#renderer?: ReturnType<typeof init>; | ||
|
||
constructor({ | ||
router, | ||
clerk, | ||
options, | ||
}: { | ||
router: ClerkHostRouter | undefined; | ||
clerk: LoadedClerk; | ||
options: ClerkOptions; | ||
}) { | ||
this.router = router; | ||
this.clerk = clerk; | ||
this.options = options; | ||
|
||
// register components | ||
this.register('SignIn', { | ||
type: 'component', | ||
load: () => | ||
import(/* webpackChunkName: "rebuild--sign-in" */ '@clerk/ui/sign-in').then(({ SignIn }) => ({ | ||
default: SignIn, | ||
})), | ||
}); | ||
this.register('SignUp', { | ||
type: 'component', | ||
load: () => | ||
import(/* webpackChunkName: "rebuild--sign-up" */ '@clerk/ui/sign-up').then(({ SignUp }) => ({ | ||
default: SignUp, | ||
})), | ||
}); | ||
} | ||
|
||
// Mount a component from the registry | ||
mount<C extends keyof ClerkNewComponents>(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}`); | ||
} | ||
|
||
// immediately start loading the component | ||
component.load(); | ||
|
||
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); | ||
} | ||
|
||
// Registers a component for rendering later | ||
register(componentName: string, componentDefinition: ComponentDefinition) { | ||
this.componentRegistry.set(componentName, componentDefinition); | ||
} | ||
|
||
renderer() { | ||
if (this.#rendererPromise) { | ||
return this.#rendererPromise.promise; | ||
} | ||
|
||
this.#rendererPromise = createDeferredPromise(); | ||
|
||
import('./renderer').then(({ init, wrapperInit }) => { | ||
assertRouter(this.router); | ||
this.#renderer = init({ | ||
wrapper: wrapperInit({ clerk: this.clerk, options: this.options, router: this.router }), | ||
}); | ||
this.#rendererPromise?.resolve(); | ||
}); | ||
|
||
return this.#rendererPromise.promise; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
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 stylesheetURLOrContent from '@clerk/ui/styles.css'; | ||
import type { ElementType, ReactNode } 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'; | ||
|
||
export function wrapperInit({ | ||
clerk, | ||
options, | ||
router, | ||
}: { | ||
clerk: LoadedClerk; | ||
options: ClerkOptions; | ||
router: ClerkHostRouter; | ||
}) { | ||
return function Wrapper({ children }: { children: ReactNode }) { | ||
return ( | ||
<ClerkInstanceContext.Provider value={{ value: clerk }}> | ||
<OptionsContext.Provider value={options}> | ||
<ClerkHostRouterContext.Provider value={router}>{children}</ClerkHostRouterContext.Provider> | ||
</OptionsContext.Provider> | ||
</ClerkInstanceContext.Provider> | ||
); | ||
}; | ||
} | ||
|
||
// Initializes the react renderer | ||
export function init({ wrapper }: { wrapper: ElementType }) { | ||
const renderedComponents = new Map<HTMLElement, [ElementType, Record<string, any>]>(); | ||
let rootElement = document.getElementById(ROOT_ELEMENT_ID); | ||
|
||
if (!rootElement) { | ||
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-injected-styles'; | ||
const existingStylesheet = document.querySelector(`[${STYLESHEET_SIGIL}]`); | ||
if (!existingStylesheet) { | ||
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); | ||
} | ||
} | ||
|
||
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( | ||
wrapper, | ||
null, | ||
Array.from(renderedComponents.entries()).map(([node, [element, props]]) => | ||
createPortal(createElement(element, props), node), | ||
), | ||
), | ||
); | ||
} | ||
|
||
function mount(element: ElementType, props: any, node: HTMLElement) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wanted to point out here that after this refactor we're probably losing the ability to This is something not blocking this PR at the moment, but I'd like to verify if that's the case and see if we actually need to ever |
||
renderedComponents.set(node, [element, props]); | ||
render(); | ||
} | ||
|
||
function unmount(node: HTMLElement) { | ||
if (!renderedComponents.has(node)) { | ||
return; | ||
} | ||
|
||
renderedComponents.delete(node); | ||
render(); | ||
} | ||
|
||
function createElementFromComponentDefinition(componentDefinition: ComponentDefinition) { | ||
return lazy(componentDefinition.load); | ||
} | ||
|
||
return { | ||
mount, | ||
unmount, | ||
createElementFromComponentDefinition, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ how's this affected