Skip to content

feat(clerk-js): Introduce legacy browser variant #5495

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
merged 16 commits into from
Apr 18, 2025
Merged
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/social-pandas-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': minor
---

Introduce `clerk.legacy.browser.js` for legacy browser support.
14 changes: 7 additions & 7 deletions packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -3,25 +3,25 @@
{ "path": "./dist/clerk.js", "maxSize": "593kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "74.2KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "100KB" },
{ "path": "./dist/vendors*.js", "maxSize": "36KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "102KB" },
{ "path": "./dist/vendors*.js", "maxSize": "39KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "38KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
{ "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },
{ "path": "./dist/organizationprofile*.js", "maxSize": "12KB" },
{ "path": "./dist/organizationswitcher*.js", "maxSize": "5KB" },
{ "path": "./dist/organizationlist*.js", "maxSize": "5.5KB" },
{ "path": "./dist/signin*.js", "maxSize": "12.5KB" },
{ "path": "./dist/signin*.js", "maxSize": "14KB" },
{ "path": "./dist/signup*.js", "maxSize": "6.75KB" },
{ "path": "./dist/userbutton*.js", "maxSize": "5KB" },
{ "path": "./dist/userprofile*.js", "maxSize": "16KB" },
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "5.28KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "5.5KB" },
{ "path": "./dist/checkout*.js", "maxSize": "3.05KB" },
{ "path": "./dist/paymentSources*.js", "maxSize": "8.2KB" },
{ "path": "./dist/paymentSources*.js", "maxSize": "8.5KB" },
{ "path": "./dist/up-billing-page*.js", "maxSize": "2.5KB" },
{ "path": "./dist/op-billing-page*.js", "maxSize": "2.5KB" },
{ "path": "./dist/sessionTasks*.js", "maxSize": "1KB" }
7 changes: 4 additions & 3 deletions packages/clerk-js/package.json
Original file line number Diff line number Diff line change
@@ -52,7 +52,7 @@
"test:coverage": "jest --collectCoverage && open coverage/lcov-report/index.html",
"watch": "rspack build --config rspack.config.js --env production --watch"
},
"browserslist": "last 2 years, Safari > 12, iOS > 12",
"browserslist": "last 2 years",
"dependencies": {
"@clerk/localizations": "workspace:^",
"@clerk/shared": "workspace:^",
@@ -70,7 +70,7 @@
"@zxcvbn-ts/language-common": "3.0.4",
"browser-tabs-lock": "1.2.15",
"copy-to-clipboard": "3.3.3",
"core-js": "3.26.1",
"core-js": "3.41.0",
"crypto-js": "^4.2.0",
"dequal": "2.0.3",
"qrcode.react": "3.1.0",
@@ -94,5 +94,6 @@
},
"publishConfig": {
"access": "public"
}
},
"browserslistLegacy": "Chrome > 73, Firefox > 66, Safari > 12, iOS > 12, Edge > 18, Opera > 58"
}
175 changes: 117 additions & 58 deletions packages/clerk-js/rspack.config.js
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ const variants = {
clerkBrowser: 'clerk.browser',
clerkHeadless: 'clerk.headless',
clerkHeadlessBrowser: 'clerk.headless.browser',
clerkLegacyBrowser: 'clerk.legacy.browser',
};

const variantToSourceFile = {
@@ -23,16 +24,18 @@ const variantToSourceFile = {
[variants.clerkBrowser]: './src/index.browser.ts',
[variants.clerkHeadless]: './src/index.headless.ts',
[variants.clerkHeadlessBrowser]: './src/index.headless.browser.ts',
[variants.clerkLegacyBrowser]: './src/index.legacy.browser.ts',
};

/**
*
* @param {object} config
* @param {'development'|'production'} config.mode
* @param {string} config.variant
* @param {boolean} [config.disableRHC=false]
* @returns { import('@rspack/cli').Configuration }
*/
const common = ({ mode, disableRHC = false }) => {
const common = ({ mode, variant, disableRHC = false }) => {
return {
mode,
resolve: {
@@ -65,7 +68,7 @@ const common = ({ mode, disableRHC = false }) => {
}),
].filter(Boolean),
output: {
chunkFilename: `[name]_[fullhash:6]_${packageJSON.version}.js`,
chunkFilename: `[name]_${variant}_[fullhash:6]_${packageJSON.version}.js`,
Copy link
Member Author

Choose a reason for hiding this comment

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

This is so that we can distinguish which variant a chunk belongs to, mainly so we could (in the future) target specific variant chunks with specific bundlewatch rules.

},
/**
* Remove the Stripe dependencies from the bundle, if RHC is disabled.
@@ -156,8 +159,10 @@ const svgLoader = () => {
};
};

/** @type { () => (import('@rspack/core').RuleSetRule[]) } */
const typescriptLoaderProd = () => {
/** @type { (opts?: { targets?: string, useCoreJs?: boolean }) => (import('@rspack/core').RuleSetRule[]) } */
const typescriptLoaderProd = (
{ targets = packageJSON.browserslist, useCoreJs = false } = { targets: packageJSON.browserslist, useCoreJs: false },
) => {
return [
{
test: /\.(jsx?|tsx?)$/,
@@ -166,7 +171,13 @@ const typescriptLoaderProd = () => {
loader: 'builtin:swc-loader',
options: {
env: {
targets: packageJSON.browserslist,
targets,
...(useCoreJs
Copy link
Member Author

Choose a reason for hiding this comment

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

Because core-js will inject polyfills even in our more modern targets, we only enable it when we know we're targeting legacy browsers.

? {
mode: 'usage',
coreJs: require('core-js/package.json').version,
}
: {}),
},
jsc: {
parser: {
@@ -188,12 +199,20 @@ const typescriptLoaderProd = () => {
},
{
test: /\.m?js$/,
exclude: /node_modules[\\/]core-js/,
use: {
loader: 'builtin:swc-loader',
options: {
env: {
targets: packageJSON.browserslist,
targets,
...(useCoreJs
? {
mode: 'usage',
coreJs: '3.41.0',
}
: {}),
},
isModule: 'unknown',
},
},
},
@@ -231,24 +250,28 @@ const typescriptLoaderDev = () => {

/**
* Used for production builds that have dynamicly loaded chunks.
* @type { () => (import('@rspack/core').Configuration) }
* @type { (opts?: { targets?: string, useCoreJs?: boolean }) => (import('@rspack/core').Configuration) }
* */
const commonForProdChunked = () => {
const commonForProdChunked = (
{ targets = packageJSON.browserslist, useCoreJs = false } = { targets: packageJSON.browserslist, useCoreJs: false },
) => {
return {
module: {
rules: [svgLoader(), ...typescriptLoaderProd()],
rules: [svgLoader(), ...typescriptLoaderProd({ targets, useCoreJs })],
},
};
};

/**
* Used for production builds that combine all files into one single file (such as for Chrome Extensions).
* @type { () => (import('@rspack/core').Configuration) }
* @type { (opts?: { targets?: string, useCoreJs?: boolean }) => (import('@rspack/core').Configuration) }
* */
const commonForProdBundled = () => {
const commonForProdBundled = (
{ targets = packageJSON.browserslist, useCoreJs = false } = { targets: packageJSON.browserslist, useCoreJs: false },
) => {
return {
module: {
rules: [svgLoader(), ...typescriptLoaderProd()],
rules: [svgLoader(), ...typescriptLoaderProd({ targets, useCoreJs })],
},
};
};
@@ -288,6 +311,14 @@ const commonForProd = () => {
// minChunkSize: 10000,
// })
],
resolve: {
alias: {
// SWC will inject imports to `core-js` into the source files that utilize polyfilled functions. Because we
// use pnpm, imports from other packages (like `packages/shared`) will not resolve. This alias forces rspack
// to resolve all `core-js` imports to the version we have installed in `clerk-js`.
'core-js': path.dirname(require.resolve('core-js/package.json')),
},
},
};
};

@@ -336,14 +367,21 @@ const prodConfig = ({ mode, env, analysis }) => {
],
}
: {},
common({ mode }),
common({ mode, variant: variants.clerkBrowser }),
commonForProd(),
commonForProdChunked(),
);

const clerkLegacyBrowser = merge(
entryForVariant(variants.clerkLegacyBrowser),
common({ mode, variant: variants.clerkLegacyBrowser }),
commonForProd(),
commonForProdChunked({ targets: packageJSON.browserslistLegacy, useCoreJs: true }),
);

const clerkHeadless = merge(
entryForVariant(variants.clerkHeadless),
common({ mode }),
common({ mode, variant: variants.clerkHeadless }),
commonForProd(),
commonForProdChunked(),
// Disable chunking for the headless variant, since it's meant to be used in a non-browser environment and
@@ -362,50 +400,62 @@ const prodConfig = ({ mode, env, analysis }) => {

const clerkHeadlessBrowser = merge(
entryForVariant(variants.clerkHeadlessBrowser),
common({ mode }),
common({ mode, variant: variants.clerkHeadlessBrowser }),
commonForProd(),
commonForProdChunked(),
// externalsForHeadless(),
);

const clerkEsm = merge(entryForVariant(variants.clerk), common({ mode }), commonForProd(), commonForProdBundled(), {
experiments: {
outputModule: true,
},
output: {
filename: '[name].mjs',
libraryTarget: 'module',
},
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 rspack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
optimization: {
splitChunks: false,
const clerkEsm = merge(
entryForVariant(variants.clerk),
common({ mode, variant: variants.clerk }),
commonForProd(),
commonForProdBundled(),
{
experiments: {
outputModule: true,
},
output: {
filename: '[name].mjs',
libraryTarget: 'module',
},
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 rspack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
optimization: {
splitChunks: false,
},
},
});
);

const clerkCjs = merge(entryForVariant(variants.clerk), common({ mode }), commonForProd(), commonForProdBundled(), {
output: {
filename: '[name].js',
libraryTarget: 'commonjs',
},
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 rspack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
optimization: {
splitChunks: false,
const clerkCjs = merge(
entryForVariant(variants.clerk),
common({ mode, variant: variants.clerk }),
commonForProd(),
commonForProdBundled(),
{
output: {
filename: '[name].js',
libraryTarget: 'commonjs',
},
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 rspack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
optimization: {
splitChunks: false,
},
},
});
);

/** @type { () => (import('@rspack/core').Configuration) } */
const commonForNoRHC = () => ({
@@ -424,7 +474,7 @@ const prodConfig = ({ mode, env, analysis }) => {

const clerkEsmNoRHC = merge(
entryForVariant(variants.clerkNoRHC),
common({ mode, disableRHC: true }),
common({ mode, disableRHC: true, variant: variants.clerkNoRHC }),
commonForProd(),
commonForProdBundled(),
commonForNoRHC(),
@@ -441,7 +491,7 @@ const prodConfig = ({ mode, env, analysis }) => {

const clerkCjsNoRHC = merge(
entryForVariant(variants.clerkNoRHC),
common({ mode, disableRHC: true }),
common({ mode, disableRHC: true, variant: variants.clerkNoRHC }),
commonForProd(),
commonForProdBundled(),
commonForNoRHC(),
@@ -458,7 +508,16 @@ const prodConfig = ({ mode, env, analysis }) => {
return [clerkBrowser];
}

return [clerkBrowser, clerkHeadless, clerkHeadlessBrowser, clerkEsm, clerkEsmNoRHC, clerkCjs, clerkCjsNoRHC];
return [
clerkBrowser,
clerkLegacyBrowser,
clerkHeadless,
clerkHeadlessBrowser,
clerkEsm,
clerkEsmNoRHC,
clerkCjs,
clerkCjsNoRHC,
];
};

/**
@@ -527,31 +586,31 @@ const devConfig = ({ mode, env }) => {
// prettier-ignore
[variants.clerk]: merge(
entryForVariant(variants.clerk),
common({ mode }),
common({ mode, variant: variants.clerk }),
commonForDev(),
),
// prettier-ignore
[variants.clerkBrowser]: merge(
entryForVariant(variants.clerkBrowser),
isSandbox ? { entry: { sandbox: './sandbox/app.ts' } } : {},
common({ mode }),
common({ mode, variant: variants.clerkBrowser }),
commonForDev(),
),
// prettier-ignore
[variants.clerkBrowserNoRHC]: merge(
entryForVariant(variants.clerkBrowserNoRHC),
common({ mode, disableRHC: true }),
common({ mode, disableRHC: true, variant: variants.clerkBrowserNoRHC }),
commonForDev(),
),
[variants.clerkHeadless]: merge(
entryForVariant(variants.clerkHeadless),
common({ mode }),
common({ mode, variant: variants.clerkHeadless }),
commonForDev(),
// externalsForHeadless(),
),
[variants.clerkHeadlessBrowser]: merge(
entryForVariant(variants.clerkHeadlessBrowser),
common({ mode }),
common({ mode, variant: variants.clerkHeadlessBrowser }),
commonForDev(),
// externalsForHeadless(),
),
2 changes: 0 additions & 2 deletions packages/clerk-js/src/index.browser.ts
Original file line number Diff line number Diff line change
@@ -3,8 +3,6 @@
// eslint-disable-next-line
import './utils/setWebpackChunkPublicPath';

import 'regenerator-runtime/runtime';

import { Clerk } from './core/clerk';

import { mountComponentRenderer } from './ui/Components';
38 changes: 38 additions & 0 deletions packages/clerk-js/src/index.legacy.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// It's crucial this is the first import,
// otherwise chunk loading will not work
// eslint-disable-next-line
import './utils/setWebpackChunkPublicPath';

import 'regenerator-runtime/runtime';

import { Clerk } from './core/clerk';

import { mountComponentRenderer } from './ui/Components';

Clerk.mountComponentRenderer = mountComponentRenderer;

const publishableKey =
document.querySelector('script[data-clerk-publishable-key]')?.getAttribute('data-clerk-publishable-key') ||
window.__clerk_publishable_key ||
'';

const proxyUrl =
document.querySelector('script[data-clerk-proxy-url]')?.getAttribute('data-clerk-proxy-url') ||
window.__clerk_proxy_url ||
'';

const domain =
document.querySelector('script[data-clerk-domain]')?.getAttribute('data-clerk-domain') || window.__clerk_domain || '';

// Ensure that if the script has already been injected we don't overwrite the existing Clerk instance.
if (!window.Clerk) {
window.Clerk = new Clerk(publishableKey, {
proxyUrl,
// @ts-expect-error
domain,
});
}

if (module.hot) {
module.hot.accept();
}
14 changes: 7 additions & 7 deletions pnpm-lock.yaml