diff --git a/examples/next-example/src/pages/_app.tsx b/examples/next-example/src/pages/_app.tsx index 9bd986e..1f398a9 100644 --- a/examples/next-example/src/pages/_app.tsx +++ b/examples/next-example/src/pages/_app.tsx @@ -3,12 +3,15 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. // +import { StyleRegistryProvider } from "@yoshiki/react/src/registry"; import type { AppProps } from "next/app"; -import { useLayoutEffect, useState } from "react"; -export default function App({ Component, pageProps }: AppProps) { - const [show, showApp] = useState(false); +const App = ({ Component, pageProps }: AppProps) => { + return ( + + + + ); +}; - useLayoutEffect(() => showApp(true)); - return show ? : null; -} +export default App; diff --git a/examples/next-example/src/pages/_document.tsx b/examples/next-example/src/pages/_document.tsx new file mode 100644 index 0000000..d8f41fb --- /dev/null +++ b/examples/next-example/src/pages/_document.tsx @@ -0,0 +1,40 @@ +// +// Copyright (c) Zoe Roux and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +// + +import { + StyleRegistryProvider, + createStyleRegistry, + useStyleRegistry, +} from "@yoshiki/react/src/registry"; +import Document, { DocumentContext } from "next/document"; + +Document.getInitialProps = async (ctx: DocumentContext) => { + const renderPage = ctx.renderPage; + const registry = createStyleRegistry(); + + ctx.renderPage = () => + renderPage({ + enhanceApp: (App) => (props) => { + return ( + + + + ); + }, + }); + + const props = await ctx.defaultGetInitialProps(ctx); + return { + ...props, + styles: ( + <> + {props.styles} + {registry.flushToComponent()} + + ), + }; +}; + +export default Document; diff --git a/package.json b/package.json index 3d947a1..00129d1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "**/react-dom", "**/react-dom/**", "**/react-native", - "**/react-native/**" + "**/react-native/**", + "**/typescript/**" ] }, "prettier": { diff --git a/packages/core/package.json b/packages/core/package.json index 2b0af27..dd25faa 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "@types/react": "^18.0.24", + "react": "^18.2.0", "typescript": "^4.8.4" } } diff --git a/packages/react/package.json b/packages/react/package.json index f2ff3a7..5c27c24 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -8,6 +8,8 @@ }, "devDependencies": { "@types/react": "^18.0.24", + "react": "^18.2.0", + "react-dom": "^18.2.0", "typescript": "^4.8.4" }, "peerDependencies": { diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index a65ec1d..4f2a0a9 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -6,6 +6,7 @@ import { Properties } from "csstype"; import { Theme, YoshikiStyle, useTheme, breakpoints, isBreakpoints } from "@yoshiki/core"; import { CSSProperties, useInsertionEffect } from "react"; +import { useStyleRegistry } from "./registry"; // TODO: shorthands export type CssObject = { @@ -50,10 +51,11 @@ const dedupProperties = (...classes: (string | undefined)[]) => { export const useYoshiki = () => { const theme = useTheme(); - let classes: string[] = []; + const registry = useStyleRegistry(); + useInsertionEffect(() => { - document.head.insertAdjacentHTML("beforeend", ``); - }, [classes]); + registry.flushToBrowser(); + }, [registry]); return { css: ( @@ -72,7 +74,7 @@ export const useYoshiki = () => { }, [[], []], ); - classes = classes.concat(localStyle); + registry.addRules(localClassNames, localStyle); return { className: dedupProperties(...localClassNames, className), style: style, diff --git a/packages/react/src/registry.tsx b/packages/react/src/registry.tsx new file mode 100644 index 0000000..1375091 --- /dev/null +++ b/packages/react/src/registry.tsx @@ -0,0 +1,96 @@ +// +// Copyright (c) Zoe Roux and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +// + +import React, { createContext, ReactNode, useContext } from "react"; + +class StyleRegistry { + private completed: string[] = []; + private rules: [string, string][] = []; + private styleElement: HTMLStyleElement | null = null; + + constructor(isDefault?: true) { + if (isDefault) { + console.warn( + "Warning: Yoshiki was used without a top level StyleRegistry. SSR won't be supported.", + ); + } + } + + addRule(key: string, rule: string) { + this.rules.push([key, rule]); + } + + addRules(keys: string[], rules: string[]) { + // I'm sad that sequence is not a thing... + for (let i = 0; i < keys.length; i++) { + this.rules.push([keys[i], rules[i]]); + } + } + + flush(): string[] { + const ret = this.rules.filter(([key]) => !this.completed.includes(key)); + console.log(ret); + this.rules = []; + this.completed.push(...ret.map(([key]) => key)); + return ret.map(([, value]) => value); + } + + flushToBrowser() { + if (!this.styleElement) { + const styles = document.querySelectorAll("style[data-yoshiki]"); + for (const style of styles) { + this.completed.push(...(style.dataset.yoshiki ?? " ").split(" ")); + if (!this.styleElement) { + this.styleElement = style; + style.dataset.yoshiki = ""; + } else { + this.styleElement.textContent = [this.styleElement.textContent, style.textContent].join( + "\n", + ); + style.remove(); + } + } + } + + const toFlush = this.flush(); + if (!toFlush.length) return; + + if (!this.styleElement) { + document.head.insertAdjacentHTML( + "beforeend", + ``, + ); + } else { + this.styleElement.textContent = [this.styleElement.textContent, ...toFlush].join("\n"); + } + } + + flushToComponent() { + const toFlush = this.flush(); + if (!toFlush.length) return null; + return ; + } +} + +const RegistryContext = createContext(null); + +export const StyleRegistryProvider = ({ + registry, + children, +}: { + registry?: StyleRegistry; + children: ReactNode; +}) => { + if (!registry && typeof window === "undefined") return children; + return ( + + {children} + + ); +}; + +export const useStyleRegistry = () => useContext(RegistryContext) || new StyleRegistry(true); + +export const createStyleRegistry = () => new StyleRegistry(); diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index e8f3a4b..aedf3ae 100755 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es6", "lib": ["dom", "dom.iterable", "esnext"], "declaration": true, "sourceMap": true, diff --git a/yarn.lock b/yarn.lock index 7296f80..57f8ff1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9509,7 +9509,7 @@ react-dom@18.0.0: loose-envify "^1.1.0" scheduler "^0.21.0" -react-dom@18.2.0: +react-dom@18.2.0, react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -9632,7 +9632,7 @@ react@18.0.0: dependencies: loose-envify "^1.1.0" -react@18.2.0: +react@18.2.0, react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==