Skip to content

Commit

Permalink
Feature/runtime performance (#25)
Browse files Browse the repository at this point in the history
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
jantimon and autofix-ci[bot] authored Nov 7, 2023
1 parent dfa3e32 commit 5eeb082
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 70 deletions.
2 changes: 1 addition & 1 deletion packages/next-yak/dist/index.cjs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"use strict";var b=Object.create;var y=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var w=Object.getPrototypeOf,O=Object.prototype.hasOwnProperty;var R=(t,e)=>{for(var s in e)y(t,s,{get:e[s],enumerable:!0})},S=(t,e,s,T)=>{if(e&&typeof e=="object"||typeof e=="function")for(let c of N(e))!O.call(t,c)&&c!==s&&y(t,c,{get:()=>e[c],enumerable:!(T=F(e,c))||T.enumerable});return t};var j=(t,e,s)=>(s=t!=null?b(w(t)):{},S(e||!t||!t.__esModule?y(s,"default",{value:t,enumerable:!0}):s,t)),v=t=>S(y({},"__esModule",{value:!0}),t);var X={};R(X,{YakThemeProvider:()=>m.YakThemeProvider,atoms:()=>C,css:()=>u,keyframes:()=>h,styled:()=>x,useTheme:()=>m.useTheme});module.exports=v(X);var E=(...t)=>{let e=[],s=[],T={};for(let a=0;a<t.length;a++){let o=t[a];if(typeof o=="string")e.push(o);else if(typeof o=="function")s.push(o);else if(typeof o=="object"&&"style"in o)for(let r in o.style){let i=o.style[r];typeof i=="function"?s.push(n=>({style:{[r]:String(d(n,i))}})):T[r]=i}}if(s.length===0){let a=e.join(" ");return()=>({className:a,style:T})}let c=(a,o,r,i)=>{let n=o(a);if(typeof n=="function")c(a,n,r,i);else if(typeof n=="object"&&n&&("className"in n&&n.className&&r.push(n.className),"style"in n&&n.style))for(let l in n.style){let f=n.style[l];i[l]=f}};return a=>{let o=[...e],r={...T};for(let i=0;i<s.length;i++)c(a,s[i],o,r);return{className:o.join(" "),style:r}}},d=(t,e)=>{let s=e(t);if(typeof s=="function")return d(t,s);if(process.env.NODE_ENV==="development"&&typeof s!="string"&&typeof s!="number"&&!(s instanceof String))throw new Error(`Dynamic CSS functions must return a string or number but returned ${JSON.stringify(s)}`);return s},u=E;var p=j(require("react"),1),k=require("next-yak/context"),B=t=>Object.assign(p.default.forwardRef(t),{component:t}),M=t=>Object.assign(A(t),{attrs:e=>A(t,e)}),A=(t,e)=>(s,...T)=>{let c=u(s,...T),a=r=>J(r,typeof e=="function"?e(r):e);return B((r,i)=>{let n=a(e||c.length?{...r,theme:(0,k.useTheme)()}:r),l=c(n),g={...typeof t=="string"?Y(n):n,style:{...n.style||{},...l.style},className:I(n.className,l.className)};return typeof t!="string"&&"yak"in t?t.yak(g,i):p.default.createElement(t,{ref:i,...g})})},x=new Proxy(M,{get(t,e){return t(e)}});function Y(t){let e={};for(let s in t)!s.startsWith("$")&&s!=="theme"&&(e[s]=t[s]);return e}var I=(t,e)=>t?e?t+" "+e:t:e,P=t=>{let e={};for(let s in t)t[s]!==void 0&&(e[s]=t[s]);return e},J=(t,e)=>e?{..."$__attrs"in t?{...P(e),...t}:{...t,...P(e)},className:I(t.className,e.className),style:{...t.style||{},...e.style||{}},$__attrs:!0}:t;var C=(...t)=>{let e=t.join(" ");return()=>({className:e})};var h=(t,...e)=>t;var m=require("next-yak/context");0&&(module.exports={YakThemeProvider,atoms,css,keyframes,styled,useTheme});
"use strict";var b=Object.create;var l=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var F=Object.getPrototypeOf,w=Object.prototype.hasOwnProperty;var O=(t,e)=>{for(var s in e)l(t,s,{get:e[s],enumerable:!0})},f=(t,e,s,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of N(e))!w.call(t,n)&&n!==s&&l(t,n,{get:()=>e[n],enumerable:!(o=h(e,n))||o.enumerable});return t};var R=(t,e,s)=>(s=t!=null?b(F(t)):{},f(e||!t||!t.__esModule?l(s,"default",{value:t,enumerable:!0}):s,t)),j=t=>f(l({},"__esModule",{value:!0}),t);var X={};O(X,{YakThemeProvider:()=>u.YakThemeProvider,atoms:()=>C,css:()=>y,keyframes:()=>I,styled:()=>k,useTheme:()=>u.useTheme});module.exports=j(X);var v=(...t)=>{let e=[],s=[],o={};for(let n of t)if(typeof n=="string")e.push(n);else if(typeof n=="function")s.push(n);else if(typeof n=="object"&&"style"in n)for(let r in n.style){let a=n.style[r];typeof a=="function"?s.push(i=>({style:{[r]:String(g(i,a))}})):o[r]=a}if(s.length===0){let n=e.join(" ");return()=>({className:n,style:o})}return n=>{let r=[...e],a={...o};for(let i=0;i<s.length;i++)E(n,s[i],r,a);return{className:r.join(" "),style:a}}},E=(t,e,s,o)=>{let n=e(t);for(;n;){if(typeof n=="function"){n=n(t);continue}else if(typeof n=="object"&&("className"in n&&n.className&&s.push(n.className),"style"in n&&n.style))for(let r in n.style)o[r]=n.style[r];break}},g=(t,e)=>{let s=e(t);if(typeof s=="function")return g(t,s);if(process.env.NODE_ENV==="development"&&typeof s!="string"&&typeof s!="number"&&!(s instanceof String))throw new Error(`Dynamic CSS functions must return a string or number but returned ${JSON.stringify(s)}`);return s},y=v;var p=R(require("react"),1),P=require("next-yak/context"),B=t=>Object.assign(p.default.forwardRef(t),{component:t}),M=t=>Object.assign(d(t),{attrs:e=>d(t,e)}),d=(t,e)=>(s,...o)=>{let n=y(s,...o),r=i=>J(i,typeof e=="function"?e(i):e);return B((i,S)=>{let c=r(Object.assign(e||n.length?{theme:(0,P.useTheme)()}:{},i)),m=n(c),T=typeof t=="string"?Y(c):c;return T.className=x(c.className,m.className),T.style="style"in c?{...c.style,...m.style}:m.style,typeof t!="string"&&"yak"in t?t.yak(T,S):(T.ref=S,p.default.createElement(t,{...T}))})},k=new Proxy(M,{get(t,e){return t(e)}});function Y(t){let e={};for(let s in t)!s.startsWith("$")&&s!=="theme"&&(e[s]=t[s]);return e}var x=(t,e)=>t?e?t+" "+e:t:e,A=t=>{let e={};for(let s in t)t[s]!==void 0&&(e[s]=t[s]);return e},J=(t,e)=>e?{..."$__attrs"in t?{...A(e),...t}:{...t,...A(e)},className:x(t.className,e.className),style:{...t.style||{},...e.style||{}},$__attrs:!0}:t;var C=(...t)=>{let e=t.join(" ");return()=>({className:e})};var I=(t,...e)=>t;var u=require("next-yak/context");0&&(module.exports={YakThemeProvider,atoms,css,keyframes,styled,useTheme});
//# sourceMappingURL=index.cjs.map
2 changes: 1 addition & 1 deletion packages/next-yak/dist/index.cjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/next-yak/dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/next-yak/dist/index.js.map

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions packages/next-yak/loaders/__tests__/tsloader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,44 @@ const FancyButton = styled(Button)\`
const FancyButton = styled(Button)(__styleYak.yak_2);"
`);
});


it("should support access to the theme", async () => {
expect(
await tsloader.call(
loaderContext,
`
import styles from "./page.module.css";
import { styled, css } from "next-yak";
const x = Math.random();
const Button = styled.button\`
font-size: 2rem;
font-weight: bold;
color: red;
\${({theme}) => theme.mode === "dark" && css\`
color: blue;
\`}
&:hover {
color: red;
}
\`;
const FancyButton = styled(Button)\`
background-color: green;
\`;
`
)
).toMatchInlineSnapshot(`
"import styles from \\"./page.module.css\\";
import { styled, css } from \\"next-yak\\";
import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
const x = Math.random();
const Button = styled.button(__styleYak.yak_0, ({
theme
}) => theme.mode === \\"dark\\" && css(__styleYak.yak_1));
const FancyButton = styled(Button)(__styleYak.yak_2);"
`);
});
});

it("should support attrs on intrinsic elements", async () => {
Expand Down
88 changes: 56 additions & 32 deletions packages/next-yak/runtime/cssLiteral.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ type CSSFunction = <TProps = {}>(
...values: CSSInterpolation<TProps & { theme: YakTheme }>[]
) => ComponentStyles<TProps>;

type PropsToClassNameFn = (props: unknown) =>
| {
className?: string;
style?: Record<string, string>;
}
| PropsToClassNameFn;

/**
* css() runtime factory of css``
*
Expand All @@ -40,25 +47,39 @@ type CSSFunction = <TProps = {}>(
const internalCssFactory = (
...args: Array<string | CSSFunction | CSSStyles<any>>
) => {
type PropsToClassNameFn = (props: unknown) => {
className?: string;
style?: Record<string, string>;
};
const classNames: string[] = [];
const dynamicCssFunctions: PropsToClassNameFn[] = [];
const style: Record<string, string> = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
for (const arg of args) {
// A CSS-module class name which got auto generated during build from static css
// e.g. css`color: red;`
// compiled -> css("yak31e4")
if (typeof arg === "string") {
classNames.push(arg);
} else if (typeof arg === "function") {
}
// Dynamic CSS e.g.
// css`${props => props.active && css`color: red;`}`
// compiled -> css((props: { active: boolean }) => props.active && css("yak31e4"))
else if (typeof arg === "function") {
dynamicCssFunctions.push(arg as unknown as PropsToClassNameFn);
} else if (typeof arg === "object" && "style" in arg) {
}
// Dynamic CSS with css variables e.g.
// css`transform: translate(${props => props.x}, ${props => props.y});`
// compiled -> css("yak31e4", { style: { "--yakVarX": props => props.x }, "--yakVarY": props => props.y }})
else if (typeof arg === "object" && "style" in arg) {
for (const key in arg.style) {
const value = arg.style[key];
if (typeof value === "function") {
dynamicCssFunctions.push((props: unknown) => ({
style: { [key]: String(recursivePropExecution(props, value)) },
style: {
[key]: String(
// The value for a css value can be a theme dependent function e.g.:
// const borderColor = (props: { theme: { mode: "dark" | "light" } }) => props.theme === "dark" ? "black" : "white";
// css`border-color: ${borderColor};`
// Therefore the value has to be extracted recursively
recursivePropExecution(props, value),
),
},
}));
} else {
style[key] = value;
Expand All @@ -73,29 +94,6 @@ const internalCssFactory = (
return () => ({ className, style });
}

// Dynamic CSS with runtime logic
const unwrapProps = (
props: unknown,
fn: PropsToClassNameFn,
classNames: string[],
style: Record<string, string>,
) => {
const result = fn(props);
if (typeof result === "function") {
unwrapProps(props, result, classNames, style);
} else if (typeof result === "object" && result) {
if ("className" in result && result.className) {
classNames.push(result.className);
}
if ("style" in result && result.style) {
for (const key in result.style) {
const value = result.style[key];
style[key] = value;
}
}
}
};

return (props: unknown) => {
const allClassNames: string[] = [...classNames];
const allStyles: Record<string, string> = { ...style };
Expand All @@ -109,6 +107,32 @@ const internalCssFactory = (
};
};

// Dynamic CSS with runtime logic
const unwrapProps = (
props: unknown,
fn: PropsToClassNameFn,
classNames: string[],
style: Record<string, string>,
) => {
let result = fn(props);
while (result) {
if (typeof result === "function") {
result = result(props);
continue;
} else if (typeof result === "object") {
if ("className" in result && result.className) {
classNames.push(result.className);
}
if ("style" in result && result.style) {
for (const key in result.style) {
style[key] = result.style[key];
}
}
}
break;
}
};

const recursivePropExecution = (
props: unknown,
fn: (props: unknown) => any,
Expand Down
67 changes: 33 additions & 34 deletions packages/next-yak/runtime/styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,24 @@ const yakStyled = <
);
const yak = (props: Substitute<TCSSProps & T, TAttrsIn>, ref: unknown) => {
/** The combined props are passed into the styled`` literal functions */
const combinedProps = processAttrs(
// if the css component does not require arguments
// it can be call without arguments and skip calling useTheme()
//
// this is NOT against the rule of hooks as
// getRuntimeStyles is a constant defined outside of the component
//
// for example
//
// const Button = styled.button`color: red;`
// ^ does not need to have access to theme
//
// const Button = styled.button`${({ theme }) => css`color: ${theme.color};`}`
// ^ must be have acces to theme
(attrs || getRuntimeStyles.length
? { ...props, theme: useTheme() }
: props) as Substitute<TCSSProps & T, TAttrsIn>,
const combinedProps: Substitute<TCSSProps & T, TAttrsIn> = processAttrs(
Object.assign(
// if the css component does not require arguments
// it can be call without arguments and skip calling useTheme()
//
// this is NOT against the rule of hooks as
// getRuntimeStyles is a constant defined outside of the component
//
// for example
//
// const Button = styled.button`color: red;`
// ^ does not need to have access to theme
//
// const Button = styled.button`${({ theme }) => css`color: ${theme.color};`}`
// ^ must be have acces to theme
attrs || getRuntimeStyles.length ? { theme: useTheme() } : {},
props,
) as Substitute<TCSSProps & T, TAttrsIn>,
);
// execute all functions inside the style literal
// e.g. styled.button`color: ${props => props.color};`
Expand All @@ -112,32 +113,30 @@ const yakStyled = <

// yak provides a className and style prop that needs to be merged with the
// user provided className and style prop
const mergedProps = {
...filteredProps,
style: {
...((combinedProps as { style?: Record<string, unknown> }).style ||
{}),
...runtimeStyles.style,
},
className: mergeClassNames(
(combinedProps as { className?: string }).className,
runtimeStyles.className as string,
),
};

(filteredProps as { className?: string }).className = mergeClassNames(
(combinedProps as { className?: string }).className,
runtimeStyles.className as string,
);
(filteredProps as { style?: React.CSSProperties }).style =
"style" in combinedProps
? {
...(combinedProps as { style?: React.CSSProperties }).style,
...runtimeStyles.style,
}
: runtimeStyles.style;
// if the styled(Component) syntax is used and the component is a yak component
// we can call the yak function directly to avoid an unnecessary wrapper with an additional
// forwardRef call
if (typeof Component !== "string" && "yak" in Component) {
return (
Component as typeof Component & {
yak: FunctionComponent<typeof mergedProps>;
yak: FunctionComponent<typeof combinedProps>;
}
).yak(mergedProps, ref);
).yak(filteredProps, ref);
}

// @ts-expect-error too complex
return <Component ref={ref as any} {...(mergedProps as any)} />;
(filteredProps as { ref?: unknown }).ref = ref;
return <Component {...(filteredProps as any)} />;
};
return yakForwardRef(yak);
};
Expand Down

0 comments on commit 5eeb082

Please sign in to comment.