-
Notifications
You must be signed in to change notification settings - Fork 431
feat(ui,react): Add shared React variant to reduce bundle size #7601
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
Merged
+681
−9
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
d3f7db8
feat(ui,react): Add shared React variant to reduce bundle size
brkalow 41e808c
docs: Add changeset for shared React variant feature
brkalow b49c8a1
refactor(react): Improve shared React variant implementation
brkalow 51b34ec
fix(clerk-js): Add missing exports for internal types and no-rhc variant
brkalow 48c4d23
fix(ui): Fix attw check for register entry point and dedupe lockfile
brkalow 7b07cc1
fix(clerk-js): Add false-cjs to attw ignore rules
brkalow c083f34
Revert "fix(clerk-js): Add false-cjs to attw ignore rules"
brkalow 6755c82
Revert "fix(clerk-js): Add missing exports for internal types and no-…
brkalow 55f725b
Apply suggestion from @brkalow
brkalow 0c4718f
Apply suggestion from @brkalow
brkalow 3a7d6b3
Apply suggestion from @brkalow
brkalow d7c670c
updates deps
brkalow 6818b53
fix(react): Fix ESLint curly brace and import sort errors
brkalow 0826067
Merge branch 'main' into brkalow/sacramento
brkalow 83fc89f
feat(ui): Add react-dom/client to shared modules registry
brkalow c84b0ef
refactor(react): Derive React version bounds from package.json peerDe…
brkalow 354950d
fix(nextjs): Pass clerkUiVariant to script URL for shared React variant
brkalow d9e7bcf
Merge branch 'main' into brkalow/sacramento
brkalow e2a5f50
refactor(react): Bundle @clerk/ui/register at build time
brkalow cbf2f9e
fix(react): Add missing VersionBounds type import in tsup config
brkalow df09090
refactor(ui): Simplify shared React externals handler with constant a…
brkalow 2cd9f97
refactor(shared): Move version check utilities to shared package
brkalow e35383c
fix lint issue
brkalow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| --- | ||
| "@clerk/ui": minor | ||
| "@clerk/react": minor | ||
| "@clerk/shared": patch | ||
| --- | ||
|
|
||
| Add shared React variant to reduce bundle size when using `@clerk/react`. | ||
|
|
||
| Introduces a new `ui.shared.browser.js` build variant that externalizes React dependencies, allowing the host application's React to be reused instead of bundling a separate copy. This can significantly reduce bundle size for applications using `@clerk/react`. | ||
|
|
||
| **New features:** | ||
| - `@clerk/ui/register` module: Import this to register React on `globalThis.__clerkSharedModules` for sharing with `@clerk/ui` | ||
| - `clerkUIVariant` option: Set to `'shared'` to use the shared variant (automatically detected and enabled for compatible React versions in `@clerk/react`) | ||
|
|
||
| **For `@clerk/react` users:** No action required. The shared variant is automatically used when your React version is compatible. | ||
|
|
||
| **For custom integrations:** Import `@clerk/ui/register` before loading the UI bundle, then set `clerkUIVariant: 'shared'` in your configuration. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| /*/ | ||
| !/src/ | ||
| !/docs/ | ||
| !/build-utils/ |
101 changes: 101 additions & 0 deletions
101
packages/react/build-utils/__tests__/parseVersionRange.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import { parseRangeToBounds, type VersionBounds } from '../parseVersionRange'; | ||
|
|
||
| describe('parseRangeToBounds', () => { | ||
| describe('caret ranges', () => { | ||
| it('parses simple caret range', () => { | ||
| expect(parseRangeToBounds('^18.0.0')).toEqual<VersionBounds[]>([[18, 0, -1, 0]]); | ||
| }); | ||
|
|
||
| it('parses caret range with non-zero minor', () => { | ||
| expect(parseRangeToBounds('^18.2.0')).toEqual<VersionBounds[]>([[18, 2, -1, 0]]); | ||
| }); | ||
|
|
||
| it('parses caret range with non-zero patch', () => { | ||
| expect(parseRangeToBounds('^18.2.5')).toEqual<VersionBounds[]>([[18, 2, -1, 5]]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('tilde ranges', () => { | ||
| it('parses simple tilde range', () => { | ||
| expect(parseRangeToBounds('~19.0.0')).toEqual<VersionBounds[]>([[19, 0, 0, 0]]); | ||
| }); | ||
|
|
||
| it('parses tilde range with non-zero minor', () => { | ||
| expect(parseRangeToBounds('~19.1.0')).toEqual<VersionBounds[]>([[19, 1, 1, 0]]); | ||
| }); | ||
|
|
||
| it('parses tilde range with non-zero patch', () => { | ||
| expect(parseRangeToBounds('~19.0.3')).toEqual<VersionBounds[]>([[19, 0, 0, 3]]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('exact versions', () => { | ||
| it('treats exact version as caret range', () => { | ||
| expect(parseRangeToBounds('18.3.1')).toEqual<VersionBounds[]>([[18, 3, -1, 1]]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('OR combinations', () => { | ||
| it('parses two caret ranges', () => { | ||
| expect(parseRangeToBounds('^18.0.0 || ^19.0.0')).toEqual<VersionBounds[]>([ | ||
| [18, 0, -1, 0], | ||
| [19, 0, -1, 0], | ||
| ]); | ||
| }); | ||
|
|
||
| it('parses mixed caret and tilde ranges', () => { | ||
| expect(parseRangeToBounds('^18.0.0 || ~19.0.3')).toEqual<VersionBounds[]>([ | ||
| [18, 0, -1, 0], | ||
| [19, 0, 0, 3], | ||
| ]); | ||
| }); | ||
|
|
||
| it('parses multiple tilde ranges', () => { | ||
| expect(parseRangeToBounds('~19.0.3 || ~19.1.4 || ~19.2.3')).toEqual<VersionBounds[]>([ | ||
| [19, 0, 0, 3], | ||
| [19, 1, 1, 4], | ||
| [19, 2, 2, 3], | ||
| ]); | ||
| }); | ||
|
|
||
| it('parses complex real-world range', () => { | ||
| // This is the actual range from pnpm-workspace.yaml | ||
| expect(parseRangeToBounds('^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0')).toEqual<VersionBounds[]>([ | ||
| [18, 0, -1, 0], | ||
| [19, 0, 0, 3], | ||
| [19, 1, 1, 4], | ||
| [19, 2, 2, 3], | ||
| [19, 3, 3, 0], | ||
| ]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('edge cases', () => { | ||
| it('handles extra whitespace', () => { | ||
| expect(parseRangeToBounds(' ^18.0.0 || ^19.0.0 ')).toEqual<VersionBounds[]>([ | ||
| [18, 0, -1, 0], | ||
| [19, 0, -1, 0], | ||
| ]); | ||
| }); | ||
|
|
||
| it('returns empty array for invalid input', () => { | ||
| expect(parseRangeToBounds('invalid')).toEqual<VersionBounds[]>([]); | ||
| expect(parseRangeToBounds('')).toEqual<VersionBounds[]>([]); | ||
| }); | ||
|
|
||
| it('skips invalid parts in OR combinations', () => { | ||
| expect(parseRangeToBounds('^18.0.0 || invalid || ^19.0.0')).toEqual<VersionBounds[]>([ | ||
| [18, 0, -1, 0], | ||
| [19, 0, -1, 0], | ||
| ]); | ||
| }); | ||
|
|
||
| it('handles prerelease versions', () => { | ||
| // semver.coerce strips prerelease info | ||
| expect(parseRangeToBounds('~19.3.0-0')).toEqual<VersionBounds[]>([[19, 3, 3, 0]]); | ||
| expect(parseRangeToBounds('^19.0.0-rc.1')).toEqual<VersionBounds[]>([[19, 0, -1, 0]]); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { coerce } from 'semver'; | ||
|
|
||
| import type { VersionBounds } from '@clerk/shared/versionCheck'; | ||
|
|
||
| export type { VersionBounds } from '@clerk/shared/versionCheck'; | ||
|
|
||
| /** | ||
| * Parses a semver range string (e.g., "^18.0.0 || ~19.0.3") into version bounds. | ||
| * | ||
| * Supported formats: | ||
| * - Caret ranges: ^X.Y.Z - allows any version >= X.Y.Z and < (X+1).0.0 | ||
| * - Tilde ranges: ~X.Y.Z - allows any version >= X.Y.Z and < X.(Y+1).0 | ||
| * - Exact versions: X.Y.Z - treated as caret range | ||
| * - OR combinations: "^18.0.0 || ~19.0.3" - multiple ranges separated by || | ||
| * | ||
| * @param rangeStr - The semver range string to parse | ||
| * @returns Array of version bounds, one per range component | ||
| */ | ||
| export function parseRangeToBounds(rangeStr: string): VersionBounds[] { | ||
| const bounds: VersionBounds[] = []; | ||
| const parts = rangeStr.split('||').map(s => s.trim()); | ||
|
|
||
| for (const part of parts) { | ||
| if (part.startsWith('^')) { | ||
| // Caret range: ^X.Y.Z means >= X.Y.Z and < (X+1).0.0 | ||
| const ver = coerce(part.slice(1)); | ||
| if (ver) { | ||
| bounds.push([ver.major, ver.minor, -1, ver.patch]); | ||
| } | ||
| } else if (part.startsWith('~')) { | ||
| // Tilde range: ~X.Y.Z means >= X.Y.Z and < X.(Y+1).0 | ||
| const ver = coerce(part.slice(1)); | ||
| if (ver) { | ||
| bounds.push([ver.major, ver.minor, ver.minor, ver.patch]); | ||
| } | ||
| } else { | ||
| // Exact version or other format - try to parse as caret | ||
| const ver = coerce(part); | ||
| if (ver) { | ||
| bounds.push([ver.major, ver.minor, -1, ver.patch]); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return bounds; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { isVersionCompatible, type VersionBounds } from '@clerk/shared/versionCheck'; | ||
| import React from 'react'; | ||
|
|
||
| export { | ||
| checkVersionAgainstBounds, | ||
| isVersionCompatible, | ||
| parseVersion, | ||
| type VersionBounds, | ||
| } from '@clerk/shared/versionCheck'; | ||
|
|
||
| declare const __CLERK_UI_SUPPORTED_REACT_BOUNDS__: VersionBounds[]; | ||
|
|
||
| /** | ||
| * Checks if the host application's React version is compatible with @clerk/ui's shared variant. | ||
| * The shared variant expects React to be provided via globalThis.__clerkSharedModules, | ||
| * so we need to ensure the host's React version matches what @clerk/ui was built against. | ||
| * | ||
| * This function is evaluated once at module load time. | ||
| */ | ||
| function computeReactVersionCompatibility(): boolean { | ||
| try { | ||
| return isVersionCompatible(React.version, __CLERK_UI_SUPPORTED_REACT_BOUNDS__); | ||
| } catch { | ||
| // If we can't determine compatibility, fall back to non-shared variant | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Whether the host React version is compatible with the shared @clerk/ui variant. | ||
| * This is computed once at module load time for optimal performance. | ||
| */ | ||
| export const IS_REACT_SHARED_VARIANT_COMPATIBLE = computeReactVersionCompatibility(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I don't love the logic here, doing some iteration...