Skip to content

Commit

Permalink
Added SsrScript to work around suspended script (#99)
Browse files Browse the repository at this point in the history
* simplified browser script

* improved code structure

* added ssr script
  • Loading branch information
fomalhautb committed Jun 25, 2024
1 parent 02568ba commit fd40498
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 52 deletions.
14 changes: 3 additions & 11 deletions packages/stack/src/components/elements/maybe-full-page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { Container } from "@stackframe/stack-ui";
import React, { useEffect, useId } from "react";
import React, { useId } from "react";
import { SsrScript } from "./ssr-layout-effect";

export function MaybeFullPage({
children,
Expand All @@ -23,13 +24,6 @@ export function MaybeFullPage({
el.style.minHeight = \`calc(100vh - \${offset}px)\`;
})(${JSON.stringify([id])})`;

useEffect(() => {
// TODO fix workaround: React has a bug where it doesn't run the script on the first CSR render if SSR has been skipped due to suspense
// As a workaround, we run the script in the <script> tag again after the first render
// Note that we do an indirect eval as described here: https://esbuild.github.io/content-types/#direct-eval
(0, eval)(scriptString);
}, []);

if (fullPage) {
return (
<>
Expand All @@ -49,9 +43,7 @@ export function MaybeFullPage({
{children}
</Container>
</div>
<script dangerouslySetInnerHTML={{
__html: scriptString,
}} />
<SsrScript script={scriptString} />
</>
);
} else {
Expand Down
13 changes: 13 additions & 0 deletions packages/stack/src/components/elements/ssr-layout-effect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use client";
import { useLayoutEffect } from "react";

export function SsrScript(props: { script: string }) {
useLayoutEffect(() => {
// TODO fix workaround: React has a bug where it doesn't run the script on the first CSR render if SSR has been skipped due to suspense
// As a workaround, we run the script in the <script> tag again after the first render
// Note that we do an indirect eval as described here: https://esbuild.github.io/content-types/#direct-eval
(0, eval)(props.script);
}, []);

return <script dangerouslySetInnerHTML={{ __html: props.script }} />;
}
40 changes: 38 additions & 2 deletions packages/stack/src/providers/theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import StyledComponentsRegistry from "./styled-components-registry";
import { globalCSS } from "../generated/global-css";
import { BrowserScript } from "../utils/browser-script";
import { DEFAULT_THEME } from "../utils/constants";
import Color from "color";
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";

type Colors = {
background: string,
Expand Down Expand Up @@ -38,6 +40,40 @@ type ThemeConfig = {
light?: Partial<Colors>,
dark?: Partial<Colors>,
} & Partial<Omit<Theme, 'light' | 'dark'>>;

function convertColorToCSSVars(obj: Record<string, string>) {
return Object.fromEntries(Object.entries(obj).map(([key, value]) => {
const color = Color(value).hsl().array();
return [
// Convert camelCase key to dash-case
key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`),
// Convert color to CSS HSL string
`${color[0]} ${color[1]}% ${color[2]}%`
];
}));
}

function convertColorsToCSS(theme: Theme) {
const { dark, light, ...rest } = theme;
const colors = {
light: { ...convertColorToCSSVars(light), ...rest },
dark: convertColorToCSSVars(dark),
};

function colorsToCSSVars(colors: Record<string, string>) {
return Object.entries(colors).map((params) => {
return `--${params[0]}: ${params[1]};\n`;
}).join('');
}

return deindent`
.stack-scope {
${colorsToCSSVars(colors.light)}
}
[data-stack-theme="dark"] .stack-scope {
${colorsToCSSVars(colors.dark)}
}`;
}


export function StackTheme({
Expand All @@ -56,8 +92,8 @@ export function StackTheme({

return (
<StyledComponentsRegistry>
<BrowserScript theme={themeValue} />
<style dangerouslySetInnerHTML={{ __html: globalCSS }} />
<BrowserScript />
<style dangerouslySetInnerHTML={{ __html: globalCSS + '\n' + convertColorsToCSS(themeValue) }} />
{children}
</StyledComponentsRegistry>
);
Expand Down
45 changes: 6 additions & 39 deletions packages/stack/src/utils/browser-script.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Color from "color";
import { Theme } from "../providers/theme-provider";

// Note that this script can not import anything from outside as it will be converted to a string and executed in the browser.

import { SsrScript } from "../components/elements/ssr-layout-effect";

// Also please note that there might be hydration issues with this script, always check the browser console for errors after changing this script.
const script = (colors: { light: Record<string, string>, dark: Record<string, string> }) => {
const script = () => {
const attributes = ['data-joy-color-scheme', 'data-mui-color-scheme', 'data-theme', 'data-color-scheme', 'class'];

const observer = new MutationObserver((mutations) => {
Expand All @@ -29,41 +29,8 @@ const script = (colors: { light: Record<string, string>, dark: Record<string, st
attributes: true,
attributeFilter: attributes,
});

function colorsToCSSVars(colors: Record<string, string>) {
return Object.entries(colors).map((params) => {
return "--" + params[0] + ": " + params[1] + ";\n";
}).join('');
}

const cssVars = '.stack-scope {\n' + colorsToCSSVars(colors.light) + '}\n[data-stack-theme="dark"] .stack-scope {\n' + colorsToCSSVars(colors.dark) + '}';
const style = document.createElement('style');
style.textContent = cssVars;
document.head.appendChild(style);
};

function convertKeysToDashCase(obj: Record<string, string>) {
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`), value]));
}

function convertColorToHSL(obj: Record<string, string>) {
return Object.fromEntries(Object.entries(obj).map(([key, value]) => {
const color = Color(value).hsl().array();
return [key, `${color[0]} ${color[1]}% ${color[2]}%`];
}));
}

function processColors(colors: Record<string, string>) {
return convertColorToHSL(convertKeysToDashCase(colors));
}

export function BrowserScript(props: { theme: Theme }) {
const { dark, light, ...rest } = props.theme;
const convertedColors = {
light: { ...processColors(light), ...rest },
dark: processColors(dark),
};
return (
<script dangerouslySetInnerHTML={{ __html: `(${script.toString()})(${JSON.stringify(convertedColors)})` }}/>
);
export function BrowserScript() {
return <SsrScript script={`(${script.toString()})()`}/>;
}

0 comments on commit fd40498

Please sign in to comment.