From 2926b6d7b441714f4b2505baa42ea2db5cc3ba9d Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:35:52 -0700 Subject: [PATCH] fix(clerk-js): Bundle CSS into CJS and ESM bundles --- 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];