Skip to content
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

Open
wants to merge 55 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
9e71651
update clerk/ui for rendering
BRKalow Sep 5, 2024
8f3ea4b
fix host router issue
BRKalow Sep 5, 2024
b3ee721
new ui renderer
BRKalow Sep 5, 2024
851f201
refactor component renderer, add sign up
BRKalow Sep 6, 2024
371f6ee
remove unused function
BRKalow Sep 6, 2024
b8d014f
add another comment
BRKalow Sep 6, 2024
56af4f5
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
BRKalow Sep 6, 2024
f0c6f51
tweak webpack chunk settings
BRKalow Sep 7, 2024
2fdd33e
fix bring back isSubmitting boolean
alexcarpenter Sep 10, 2024
5322329
update sideEffects to enable loading css file
alexcarpenter Sep 10, 2024
b9b3359
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
alexcarpenter Sep 10, 2024
e3132e3
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Sep 19, 2024
f000689
feat(clerk-js,types,shared): Add necessary types for new UI
dstaley Sep 24, 2024
9866b16
chore(repo): Add changeset
dstaley Sep 24, 2024
812035a
fix(clerk-js): Add dependency on @clerk/ui
dstaley Sep 24, 2024
da7ba53
chore(clerk-js): Temporarily increase bundlewatch limits
dstaley Sep 24, 2024
3fe0bda
chore(clerk-js): Temporarily increase bundlewatch limits
dstaley Sep 24, 2024
9542bab
fix(clerk-js): Ensure CJS script doesn't contain ESM code
dstaley Sep 24, 2024
a75375f
fix(ui): Mark package as public
dstaley Sep 24, 2024
7f71197
fix(nextjs): Pass correct router option
dstaley Sep 24, 2024
31d749c
fix(clerk-js): Emit CJS chunks as CJS
dstaley Sep 24, 2024
4b6c093
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Sep 24, 2024
86419e0
fix(clerk-js): Only instantiate new UI if Clerk is loaded
dstaley Sep 24, 2024
93c17d2
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Sep 24, 2024
e264672
fix(clerk-js): Remove scriptType config
dstaley Sep 25, 2024
4e84b25
fix(clerk-js): Exclude ui and elements package from ui-common chunk
dstaley Sep 25, 2024
4b50d86
fix(clerk-js): Consolidate wrapper and renderer into single chunk
dstaley Sep 25, 2024
02ac475
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Sep 25, 2024
c245d6f
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Oct 1, 2024
2aab6cd
Merge branch 'refs/heads/main' into brk.feat/render-ui-in-clerk-js
nikosdouvlis Oct 2, 2024
8ab9b83
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
nikosdouvlis Oct 2, 2024
368db81
fix(clerk-js): Remove duplicate dependencies
dstaley Oct 2, 2024
b40c293
fix(clerk-js): Add types for mount call
dstaley Oct 2, 2024
fc3f448
fix(clerk-js): Remove duplicate imports
dstaley Oct 2, 2024
d415f19
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Oct 2, 2024
477ee05
fix(clerk-js): Pass __experimental_router to pages router provider
dstaley Oct 2, 2024
860897c
feat(ui): Add `<RouterLink />` component (#4277)
alexcarpenter Oct 3, 2024
2cc9acd
chore(elements): Align clerk dependencies (#4282)
panteliselef Oct 4, 2024
07a26ab
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Oct 4, 2024
77bd3e0
feat(types,ui): Add support for `__experimental.appearance` prop (#4290)
dstaley Oct 4, 2024
6bae6c3
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Oct 4, 2024
3d6d9ae
fix(nextjs): Do not export useNextRouter
dstaley Oct 4, 2024
74c81bf
feat(clerk-js): Add support for loading UI styles as first CSS styles…
dstaley Oct 7, 2024
63f97d3
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Oct 7, 2024
4746fb4
fix(clerk-js): Ensure import.meta.url isn't used in CJS build
dstaley Oct 7, 2024
54a836c
fix(clerk-js): Restore minimization
dstaley Oct 7, 2024
29258d0
fix(clerk-js): Bundle CSS into CJS and ESM bundles (#4301)
dstaley Oct 8, 2024
c65ea96
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Oct 8, 2024
bab0ad3
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
BRKalow Oct 11, 2024
129bc20
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
jacekradko Oct 11, 2024
98fb7db
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Oct 15, 2024
62dbb34
fix(ui): Fix package.json
dstaley Oct 15, 2024
dc4149e
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Oct 15, 2024
163ef01
fix(elements): Move shared back to dependencies
dstaley Oct 16, 2024
5035d98
Merge branch 'main' into brk.feat/render-ui-in-clerk-js
dstaley Oct 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/nervous-guests-guess.md
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"files": [
{ "path": "./dist/clerk.browser.js", "maxSize": "64.5kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "43kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "68kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "44kB" },
{ "path": "./dist/ui-common*.js", "maxSize": "86KB" },
{ "path": "./dist/vendors*.js", "maxSize": "70KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "58KB" },
Expand All @@ -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" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ how's this affected

]
}
1 change: 1 addition & 0 deletions packages/clerk-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@clerk/localizations": "3.1.2",
"@clerk/shared": "2.9.0",
"@clerk/types": "4.25.0",
"@clerk/ui": "0.1.9",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] is @clerk/ui considered pre-release since it's still 0.x.x?

Copy link
Member

Choose a reason for hiding this comment

The 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 "*". I'll let @BRKalow confirm though.

The ui package is bundled via Webpack into clerk-js, so we don't need it published to npm in order to consume it.

"@coinbase/wallet-sdk": "4.0.4",
"@emotion/cache": "11.11.0",
"@emotion/react": "11.11.1",
Expand Down
59 changes: 41 additions & 18 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
HandleOAuthCallbackParams,
InstanceType,
ListenerCallback,
LoadedClerk,
NavigateOptions,
OrganizationListProps,
OrganizationProfileProps,
Expand Down Expand Up @@ -64,6 +65,7 @@ import type {
} from '@clerk/types';

import type { MountComponentRenderer } from '../ui/Components';
import { UI } from '../ui/new';
import {
ALLOWED_PROTOCOLS,
buildURL,
Expand Down Expand Up @@ -145,7 +147,12 @@ const defaultOptions: ClerkOptions = {
signUpForceRedirectUrl: undefined,
};

function clerkIsLoaded(clerk: ClerkInterface): clerk is LoadedClerk {
return !!clerk.client;
}

export class Clerk implements ClerkInterface {
public __experimental_ui?: UI;
public static mountComponentRenderer?: MountComponentRenderer;

public static version: string = __PKG_VERSION__;
Expand Down Expand Up @@ -317,6 +324,14 @@ export class Clerk implements ClerkInterface {
} else {
this.#loaded = await this.#loadInNonStandardBrowser();
}

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) => {
Expand Down Expand Up @@ -495,15 +510,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 && 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 =>
controls.mountComponent({
name: 'SignIn',
appearanceKey: 'signIn',
node,
props,
}),
);
}
this.telemetry?.record(eventPrebuiltComponentMounted('SignIn', props));
};

Expand All @@ -517,15 +536,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 && 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 =>
controls.mountComponent({
name: 'SignUp',
appearanceKey: 'signUp',
node,
props,
}),
);
}
this.telemetry?.record(eventPrebuiltComponentMounted('SignUp', props));
};

Expand Down
98 changes: 98 additions & 0 deletions packages/clerk-js/src/ui/new/index.tsx
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;
}
}
87 changes: 87 additions & 0 deletions packages/clerk-js/src/ui/new/renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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 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);
}

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) {
Copy link
Member

Choose a reason for hiding this comment

The 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 await the mount call.

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 await it or not

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,
};
}
12 changes: 12 additions & 0 deletions packages/clerk-js/src/ui/new/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +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;
};
Loading
Loading