Skip to content

Commit

Permalink
prevent passing theme to a styled component (#70)
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>
Co-authored-by: Luca Schneider <[email protected]>
  • Loading branch information
3 people authored Feb 21, 2024
1 parent 5b58aad commit 1844d86
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 38 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 l=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var F=Object.getPrototypeOf,O=Object.prototype.hasOwnProperty;var w=(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))!O.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 J={};w(J,{YakThemeProvider:()=>u.YakThemeProvider,atoms:()=>x,css:()=>y,keyframes:()=>I,styled:()=>k,useTheme:()=>u.useTheme});module.exports=j(J);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 i=n.style[r];typeof i=="function"?s.push(a=>({style:{[r]:String(g(a,i))}})):o[r]=i}if(s.length===0){let n=e.join(" ");return()=>({className:n,style:o})}return n=>{let r=[...e],i={...o};for(let a=0;a<s.length;a++)E(n,s[a],r,i);return{className:r.join(" "),style:i}}},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"),_=t=>Object.assign(p.default.forwardRef(t),{component:t}),B=t=>Object.assign(d(t),{attrs:e=>d(t,e)}),d=(t,e)=>(s,...o)=>{let n=y(s,...o),r=a=>Y(a,typeof e=="function"?e(a):e);return _((a,S)=>{let c=r(Object.assign(e||n.length?{theme:(0,P.useTheme)()}:{},a)),m=n(c),T=typeof t=="string"?M(c):c;return T.className=C(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(B,{get(t,e){return t(e)}});function M(t){let e={};for(let s in t)!s.startsWith("$")&&s!=="theme"&&(e[s]=t[s]);return e}var C=(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},Y=(t,e)=>e?{..."$__attrs"in t?{...A(e),...t}:{...t,...A(e)},className:C(t.className,e.className),style:{...t.style||{},...e.style||{}},$__attrs:!0}:t;var x=(...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});
"use strict";var F=Object.create;var T=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var R=Object.getPrototypeOf,j=Object.prototype.hasOwnProperty;var v=(t,e)=>{for(var s in e)T(t,s,{get:e[s],enumerable:!0})},d=(t,e,s,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of O(e))!j.call(t,n)&&n!==s&&T(t,n,{get:()=>e[n],enumerable:!(o=w(e,n))||o.enumerable});return t};var E=(t,e,s)=>(s=t!=null?F(R(t)):{},d(e||!t||!t.__esModule?T(s,"default",{value:t,enumerable:!0}):s,t)),_=t=>d(T({},"__esModule",{value:!0}),t);var $={};v($,{YakThemeProvider:()=>y.YakThemeProvider,atoms:()=>h,css:()=>l,keyframes:()=>b,styled:()=>x,useTheme:()=>y.useTheme});module.exports=_($);var B=(...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 c=n.style[r];typeof c=="function"?s.push(a=>({style:{[r]:String(A(a,c))}})):o[r]=c}if(s.length===0){let n=e.join(" ");return()=>({className:n,style:o})}return n=>{let r=[...e],c={...o};for(let a=0;a<s.length;a++)M(n,s[a],r,c);return{className:r.join(" "),style:c}}},M=(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}},A=(t,e)=>{let s=e(t);if(typeof s=="function")return A(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},l=B;var p=E(require("react"),1),C=require("next-yak/context"),Y={},J=t=>Object.assign(p.default.forwardRef(t),{component:t}),X=t=>Object.assign(P(t),{attrs:e=>P(t,e)}),P=(t,e)=>(s,...o)=>{let n=l(s,...o),r=a=>K(a,typeof e=="function"?e(a):e);return J((a,S)=>{let f=e||n.length?(0,C.useTheme)():Y,m=r({theme:f,...a}),u=n(m),{theme:N,...g}=m,i=typeof t=="string"?H(g):N===f?g:m;return i.className=I(i.className,u.className),i.style="style"in i?{...i.style,...u.style}:u.style,typeof t!="string"&&"yak"in t?t.yak(i,S):(i.ref=S,p.default.createElement(t,{...i}))})},x=new Proxy(X,{get(t,e){return t(e)}});function H(t){let e={};for(let s in t)s.startsWith("$")||(e[s]=t[s]);return e}var I=(t,e)=>t?e?t+" "+e:t:e,k=t=>{let e={};for(let s in t)t[s]!==void 0&&(e[s]=t[s]);return e},K=(t,e)=>e?{..."$__attrs"in t?{...k(e),...t}:{...t,...k(e)},className:I(t.className,e.className),style:{...t.style||{},...e.style||{}},$__attrs:!0}:t;var h=(...t)=>{let e=t.join(" ");return()=>({className:e})};var b=(t,...e)=>t;var y=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.

2 changes: 1 addition & 1 deletion packages/next-yak/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "next-yak",
"version": "0.0.32",
"version": "0.0.34",
"type": "module",
"types": "./dist/",
"exports": {
Expand Down
54 changes: 52 additions & 2 deletions packages/next-yak/runtime/__tests__/attrs.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { it, expect, vi, beforeEach } from "vitest";
import React, { FunctionComponent } from "react";
import TestRenderer from "react-test-renderer";
import { beforeEach, expect, it, vi } from "vitest";
import { YakThemeProvider } from "../context";
import { styled } from "../styled";
import React, { FunctionComponent } from "react";

beforeEach(() => {
vi.spyOn(console, "warn");
Expand Down Expand Up @@ -488,3 +489,52 @@ it("should have optional attrs props as component interface", () => {
/>
`);
});

it("should have access to theme", () => {
const ThemePrinter = ({ theme, ...props }: { theme?: unknown }) => (
<pre {...props}>{JSON.stringify(theme)}</pre>
);
const Comp = styled(ThemePrinter).attrs<DataAttributes>((p) => ({
"data-color": (p.theme as { color: string }).color,
}))``;

expect(
TestRenderer.create(
<YakThemeProvider theme={{ color: "red" }}>
<Comp />
</YakThemeProvider>,
).toJSON(),
).toMatchInlineSnapshot(`
<pre
$__attrs={true}
className=""
data-color="red"
style={{}}
/>
`);
});

it("should pass theme if theme is overwritten", () => {
const ThemePrinter = ({ theme, ...props }: { theme?: unknown }) => (
<pre {...props}>{JSON.stringify(theme)}</pre>
);
const Comp = styled(ThemePrinter).attrs({
theme: { color: "blue" },
})``;

expect(
TestRenderer.create(
<YakThemeProvider theme={{ color: "red" }}>
<Comp />
</YakThemeProvider>,
).toJSON(),
).toMatchInlineSnapshot(`
<pre
$__attrs={true}
className=""
style={{}}
>
{"color":"blue"}
</pre>
`);
});
95 changes: 90 additions & 5 deletions packages/next-yak/runtime/__tests__/styled.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/// @ts-nocheck
// @ts-nocheck
// We are testing internal functionality which does not match
// 1:1 the API exposed to the user before compilation.
// Therfefore types are not matching and need to be ignored.

import { it, expect } from "vitest";
import { render } from "@testing-library/react";
import { styled } from "../styled";
import { css } from "../cssLiteral";
import React from "react";
import { expect, it } from "vitest";
import { YakThemeProvider } from "../context";
import { css } from "../cssLiteral";
import { styled } from "../styled";

it("should render a literal element", () => {
const Component = styled.input``;
Expand Down Expand Up @@ -223,3 +223,88 @@ it("should allow using nested refs", () => {

expect(elementFromRef).toBeInstanceOf(HTMLInputElement);
});

it("should remove theme if styled element", () => {
const Link = styled.a((p) => p && css("test"));

const { container } = render(
<YakThemeProvider theme={{ color: "red" }}>
<Link />
</YakThemeProvider>,
);

expect(container).toMatchInlineSnapshot(`
<div>
<a
class="test"
/>
</div>
`);
});

it("should not remove theme if theme is passed to element", () => {
const ThemePrinter = ({ theme, ...props }: { theme?: unknown }) => (
<pre {...props}>{JSON.stringify(theme)}</pre>
);
const Link = styled(ThemePrinter)((p) => p && css("test"));

const { container } = render(
<YakThemeProvider theme={{ color: "red" }}>
<Link theme={{ anything: "test" }} />
</YakThemeProvider>,
);

expect(container).toMatchInlineSnapshot(`
<div>
<pre
class="test"
>
{"anything":"test"}
</pre>
</div>
`);
});

it("should remove theme on wrapped element", () => {
const BaseComponent = styled.input((p) => p && css("test"));
const Component = styled(BaseComponent)((p) => p && css("test-wrapper"));

const { container } = render(
<YakThemeProvider theme={{ color: "red" }}>
<Component />
</YakThemeProvider>,
);

expect(container).toMatchInlineSnapshot(`
<div>
<input
class="test-wrapper test"
/>
</div>
`);
});

it("should not remove theme if theme is passed to wrapped element", () => {
const ThemePrinter = ({ theme, ...props }: { theme?: unknown }) => (
<pre {...props}>{JSON.stringify(theme)}</pre>
);

const BaseComponent = styled(ThemePrinter)((p) => p && css("test"));
const Component = styled(BaseComponent)((p) => p && css("test-wrapper"));

const { container } = render(
<YakThemeProvider theme={{ color: "red" }}>
<Component theme={{ anything: "test" }} />
</YakThemeProvider>,
);

expect(container).toMatchInlineSnapshot(`
<div>
<pre
class="test-wrapper test"
>
{"anything":"test"}
</pre>
</div>
`);
});
62 changes: 36 additions & 26 deletions packages/next-yak/runtime/styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import React from "react";
import { useTheme } from "next-yak/context";
import type { YakTheme } from "./context/index.d.ts";

/** Symbol */
const noTheme = {};

/**
* Hack to hide .yak from the type definition and to deal with ExoticComponents
*/
Expand Down Expand Up @@ -85,47 +88,54 @@ const yakStyled = <
typeof attrs === "function" ? (attrs as Function)(props) : attrs,
);
const yak = (props: Substitute<TCSSProps & T, TAttrsIn>, ref: unknown) => {
/** The combined props are passed into the styled`` literal functions */
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()
//
// `__yak` 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>,
);
// if the css component does not require arguments
// it can be call without arguments and skip calling useTheme()
//
// `__yak` 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
const theme = attrs || getRuntimeStyles.length ? useTheme() : noTheme;
// execute attrs
const combinedProps: Substitute<TCSSProps & T, TAttrsIn> = processAttrs({
theme,
...props,
} as Substitute<TCSSProps & T, TAttrsIn>);
// execute all functions inside the style literal
// e.g. styled.button`color: ${props => props.color};`
const runtimeStyles = getRuntimeStyles(combinedProps as any);

// delete the yak theme from the props
// this must happen after the runtimeStyles are calculated
// prevents passing the theme prop to the DOM element of a styled component
const { theme: themeAfterAttr, ...combinedPropsWithoutTheme } =
combinedProps as { theme?: unknown };

// remove all props that start with a $ sign for string components e.g. "button" or "div"
// so that they are not passed to the DOM element
const filteredProps =
typeof Component === "string"
? removePrefixedProperties(combinedProps)
? removePrefixedProperties(combinedPropsWithoutTheme)
: themeAfterAttr === theme
? combinedPropsWithoutTheme
: combinedProps;

// yak provides a className and style prop that needs to be merged with the
// user provided className and style prop
(filteredProps as { className?: string }).className = mergeClassNames(
(combinedProps as { className?: string }).className,
(filteredProps as { className?: string }).className,
runtimeStyles.className as string,
);
(filteredProps as { style?: React.CSSProperties }).style =
"style" in combinedProps
"style" in filteredProps
? {
...(combinedProps as { style?: React.CSSProperties }).style,
...(filteredProps as { style?: React.CSSProperties }).style,
...runtimeStyles.style,
}
: runtimeStyles.style;
Expand All @@ -135,7 +145,7 @@ const yakStyled = <
if (typeof Component !== "string" && "yak" in Component) {
return (
Component as typeof Component & {
yak: FunctionComponent<typeof combinedProps>;
yak: FunctionComponent<typeof filteredProps>;
}
).yak(filteredProps, ref);
}
Expand Down Expand Up @@ -199,7 +209,7 @@ export const styled = new Proxy(
function removePrefixedProperties<T extends Record<string, unknown>>(obj: T) {
const result = {} as T;
for (const key in obj) {
if (!key.startsWith("$") && key !== "theme") {
if (!key.startsWith("$")) {
result[key] = obj[key];
}
}
Expand Down

0 comments on commit 1844d86

Please sign in to comment.