diff options
Diffstat (limited to '@linaria/packages/react')
-rw-r--r-- | @linaria/packages/react/CHANGELOG.md | 46 | ||||
-rw-r--r-- | @linaria/packages/react/README.md | 35 | ||||
-rw-r--r-- | @linaria/packages/react/__dtslint__/index.d.ts | 2 | ||||
-rw-r--r-- | @linaria/packages/react/__dtslint__/styled.ts | 135 | ||||
-rw-r--r-- | @linaria/packages/react/__dtslint__/tsconfig.json | 14 | ||||
-rw-r--r-- | @linaria/packages/react/__tests__/__snapshots__/styled.test.js.snap | 143 | ||||
-rw-r--r-- | @linaria/packages/react/__tests__/detect-core-js.test.js | 35 | ||||
-rw-r--r-- | @linaria/packages/react/__tests__/styled.test.js | 256 | ||||
-rw-r--r-- | @linaria/packages/react/babel.config.js | 3 | ||||
-rw-r--r-- | @linaria/packages/react/package.json | 53 | ||||
-rw-r--r-- | @linaria/packages/react/src/index.ts | 2 | ||||
-rw-r--r-- | @linaria/packages/react/src/styled.ts | 224 | ||||
-rw-r--r-- | @linaria/packages/react/tsconfig.json | 8 |
13 files changed, 956 insertions, 0 deletions
diff --git a/@linaria/packages/react/CHANGELOG.md b/@linaria/packages/react/CHANGELOG.md new file mode 100644 index 0000000..6fc0719 --- /dev/null +++ b/@linaria/packages/react/CHANGELOG.md @@ -0,0 +1,46 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.0.0-beta.11](https://github.com/callstack/linaria/compare/v3.0.0-beta.10...v3.0.0-beta.11) (2021-08-08) + + +### Bug Fixes + +* **styled:** remove unnecessary core-js polyfills (fixes [#799](https://github.com/callstack/linaria/issues/799)) ([#814](https://github.com/callstack/linaria/issues/814)) ([6c3070a](https://github.com/callstack/linaria/commit/6c3070a47715022eb761567b8795f6918784ae4c)) + + + + + +# [3.0.0-beta.7](https://github.com/callstack/linaria/compare/v3.0.0-beta.6...v3.0.0-beta.7) (2021-06-24) + +**Note:** Version bump only for package @linaria/react + + + + + +# [3.0.0-beta.4](https://github.com/callstack/linaria/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2021-05-07) + +**Note:** Version bump only for package @linaria/react + + + + + +# [3.0.0-beta.3](https://github.com/callstack/linaria/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2021-04-20) + + +### Bug Fixes + +* **core,react:** make IE 11 compatible (fixes [#746](https://github.com/callstack/linaria/issues/746)) ([#750](https://github.com/callstack/linaria/issues/750)) ([922df95](https://github.com/callstack/linaria/commit/922df9576a430cdfe9b27aed5dc45c4f75917607)) + + + + + +# [3.0.0-beta.2](https://github.com/callstack/linaria/compare/v3.0.0-beta.1...v3.0.0-beta.2) (2021-04-11) + +**Note:** Version bump only for package @linaria/react diff --git a/@linaria/packages/react/README.md b/@linaria/packages/react/README.md new file mode 100644 index 0000000..0d75b37 --- /dev/null +++ b/@linaria/packages/react/README.md @@ -0,0 +1,35 @@ +<p align="center"> + <img alt="Linaria" src="https://raw.githubusercontent.com/callstack/linaria/HEAD/website/assets/linaria-logo@2x.png" width="496"> +</p> + +<p align="center"> +Zero-runtime CSS in JS library. +</p> + +--- + +### 📖 Please refer to the [GitHub](https://github.com/callstack/linaria#readme) for full documentation. + +## Features + +- Write CSS in JS, but with **zero runtime**, CSS is extracted to CSS files during build +- Familiar **CSS syntax** with Sass like nesting +- Use **dynamic prop based styles** with the React bindings, uses CSS variables behind the scenes +- Easily find where the style was defined with **CSS sourcemaps** +- **Lint your CSS** in JS with [stylelint](https://github.com/stylelint/stylelint) +- Use **JavaScript for logic**, no CSS preprocessor needed +- Optionally use any **CSS preprocessor** such as Sass or PostCSS + +**[Why use Linaria](../../docs/BENEFITS.md)** + +## Installation + +```sh +npm install @linaria/core @linaria/react @linaria/babel-preset @linaria/shaker +``` + +or + +```sh +yarn add @linaria/core @linaria/react @linaria/babel-preset @linaria/shaker +``` diff --git a/@linaria/packages/react/__dtslint__/index.d.ts b/@linaria/packages/react/__dtslint__/index.d.ts new file mode 100644 index 0000000..b57206b --- /dev/null +++ b/@linaria/packages/react/__dtslint__/index.d.ts @@ -0,0 +1,2 @@ +// dtslint wants to see index.d.ts. Well, here it is. +declare const linaria: any; diff --git a/@linaria/packages/react/__dtslint__/styled.ts b/@linaria/packages/react/__dtslint__/styled.ts new file mode 100644 index 0000000..446d3b4 --- /dev/null +++ b/@linaria/packages/react/__dtslint__/styled.ts @@ -0,0 +1,135 @@ +/* tslint:disable:no-unnecessary-generics */ +// eslint-disable-next-line import/no-extraneous-dependencies +import * as React from 'react'; +import { css } from '@linaria/core'; +import { styled } from '../src'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function isExtends<C, T>(arg1?: C, arg2?: T): C extends T ? 'extends' : never { + // It will never be executed, so the result doesn't matter. + return null as any; +} + +const Fabric = + <T>(): React.FC<T> => + (props) => + React.createElement('div', props); + +const Header = (p: { children: string }) => React.createElement('h1', p); + +const Generic = <T>(p: T & { className?: string }) => + React.createElement('h1', p); + +const StyledDiv = styled.div``; +// $ExpectType "extends" +isExtends<typeof StyledDiv, React.FC<React.DetailedHTMLProps<any, any>>>(); + +// component should have className property +// $ExpectError +styled(Fabric<{ a: string }>())``; + +// className property should be string +// $ExpectError +styled(Fabric<{ className: number }>())``; + +const SimplestComponent = styled(Fabric<{ className: string }>())``; +// $ExpectType "extends" +isExtends<typeof SimplestComponent, React.FC<{ className: string }>>(); + +styled(Fabric<{ className: string }>())` + // component should have style property + // $ExpectError + color: ${() => 'red'}; +`; + +styled(Fabric<{ className: string }>())` + // it looks like function, but it's a reference to another styled component + & > ${SimplestComponent} { + color: red; + } +`; + +styled(Fabric<{ className: string }>())` + // it looks like the previous test, but it references a non-linaria component + // $ExpectError + & > ${Header} { + color: red; + } +`; + +styled(Fabric<{ className: string; style: {} }>())` + color: ${() => 'red'}; +`; + +styled(Fabric<{ className: string; style: {} }>())` + // color should be defined in props + // $ExpectError + color: ${(props) => props.color}; +`; + +styled(Fabric<{ className: string; style: {}; color: 'red' | 'blue' }>())` + & > ${SimplestComponent} { + color: ${(props) => props.color}; + } +`; + +// $ExpectType number +Generic({ children: 123 }).props.children; + +const StyledGeneric = styled(Generic)``; +// $ExpectType number +StyledGeneric({ children: 123 }).props.children; + +styled.a` + & > ${SimplestComponent} { + color: red; + } +`({ href: 'about:blank' }); + +((/* Issue #536 */) => { + const Title = styled.div<{ background: string }>` + background: ${(props) => props.background}; + `; + + // $ExpectType "extends" + isExtends<typeof Title, React.FC<{ background: string }>>(); + + css` + ${Title} { + color: green; + } + `; +})(); + +((/* Issue #622 */) => { + const Wrapper = styled.div<{ prop1: boolean }>` + width: 1em; + background-color: ${(props) => (props.prop1 ? 'transparent' : 'green')}; + `; + + const Custom: React.FC<{ className?: string; id: number }> = () => null; + + const tag = styled(Custom); + const Card = tag` + ${Wrapper} { + color: green; + } + `; + + // $ExpectType Validator<number> | undefined + Card.propTypes!.id; + + const styledTag = styled(Wrapper); + + const NewWrapper = styledTag<{ prop2: string }>` + width: 2em; + background-color: ${(props) => (props.prop1 ? 'transparent' : 'red')}; + color: ${(props) => props.prop2}; + `; + + // $ExpectType Validator<boolean> | undefined + NewWrapper.propTypes!.prop1; + + // $ExpectType Validator<string> | undefined + NewWrapper.propTypes!.prop2; +})(); diff --git a/@linaria/packages/react/__dtslint__/tsconfig.json b/@linaria/packages/react/__dtslint__/tsconfig.json new file mode 100644 index 0000000..dcb273f --- /dev/null +++ b/@linaria/packages/react/__dtslint__/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": ["es6"], + "esModuleInterop": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noEmit": true, + + "baseUrl": "./" + } +} diff --git a/@linaria/packages/react/__tests__/__snapshots__/styled.test.js.snap b/@linaria/packages/react/__tests__/__snapshots__/styled.test.js.snap new file mode 100644 index 0000000..e4cd67b --- /dev/null +++ b/@linaria/packages/react/__tests__/__snapshots__/styled.test.js.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`applies CSS variables in style prop 1`] = ` +<div + className="abcdefg" + size={24} + style={ + Object { + "--bar": "20px", + "--baz": "24px", + "--foo": "tomato", + } + } +> + This is a test +</div> +`; + +exports[`does not filter attributes for components 1`] = ` +<div> + voila +</div> +`; + +exports[`does not filter attributes for custom elements 1`] = ` +<my-element + className="abcdefg" + unknownAttribute="voila" +> + This is a test +</my-element> +`; + +exports[`filters unknown html attributes for HTML tag 1`] = ` +<div + className="abcdefg" +> + This is a test +</div> +`; + +exports[`forwards as prop when wrapping another styled component 1`] = ` +<a + className="hijklmn abcdefg" +> + This is a test +</a> +`; + +exports[`handles wrapping another styled component 1`] = ` +<div + className="hijklmn abcdefg" +> + This is a test +</div> +`; + +exports[`merges CSS variables with custom style prop 1`] = ` +<div + className="abcdefg" + style={ + Object { + "--foo": "tomato", + "bar": "baz", + } + } +> + This is a test +</div> +`; + +exports[`provides linaria component className for composition as last item in props.className 1`] = ` +<div + className="some-another-class abcdefg abcdefg--primary abcdefg--accessibility" +> + original classname used for composition +</div> +`; + +exports[`renders component with display name and class name 1`] = ` +<div + className="abcdefg" +> + This is a test +</div> +`; + +exports[`renders tag with display name and class name 1`] = ` +<h1 + className="abcdefg" +> + This is a test +</h1> +`; + +exports[`replaces custom component with as prop for primitive 1`] = ` +<a + className="abcdefg" + id="test" +> + This is a test +</a> +`; + +exports[`replaces primitive with as prop for custom component 1`] = ` +<div + className="abcdefg" + foo="bar" + id="test" + style={ + Object { + "fontSize": 12, + } + } +> + This is a test +</div> +`; + +exports[`replaces simple component with as prop 1`] = ` +<a + className="abcdefg" + id="test" +> + This is a test +</a> +`; + +exports[`supports extra class prop 1`] = ` +<div + className="primary abcdefg" +> + This is a test +</div> +`; + +exports[`supports extra className prop 1`] = ` +<div + className="primary abcdefg" +> + This is a test +</div> +`; diff --git a/@linaria/packages/react/__tests__/detect-core-js.test.js b/@linaria/packages/react/__tests__/detect-core-js.test.js new file mode 100644 index 0000000..22bfbe4 --- /dev/null +++ b/@linaria/packages/react/__tests__/detect-core-js.test.js @@ -0,0 +1,35 @@ +import cp from 'child_process'; + +const waitForProcess = async (process) => { + return new Promise((resolve) => { + let output = ''; + process.stdout.on('data', (chunk) => { + output += chunk.toString(); + }); + process.on('close', () => { + resolve(output); + }); + }); +}; + +it('Ensures that package do not include core-js dependency after build', async () => { + // eslint-disable-next-line import/no-extraneous-dependencies + const packageJSON = require('@linaria/babel-preset/package.json'); + const buildScript = packageJSON.scripts['build:lib']; + + const proc = cp.exec(buildScript, { + stdio: 'ignore', + env: { + ...process.env, + DEBUG_CORE_JS: 'true', + }, + }); + const result = await waitForProcess(proc); + // run `DEBUG_CORE_JS=true yarn build:lib` to debug issues with introduced core-js dependency + expect(result).not.toContain( + 'The corejs3 polyfill added the following polyfills' + ); + expect(result).toContain( + 'Based on your code and targets, the corejs3 polyfill did not add any polyfill' + ); +}, 15000); diff --git a/@linaria/packages/react/__tests__/styled.test.js b/@linaria/packages/react/__tests__/styled.test.js new file mode 100644 index 0000000..8f38ab2 --- /dev/null +++ b/@linaria/packages/react/__tests__/styled.test.js @@ -0,0 +1,256 @@ +const React = require('react'); +const renderer = require('react-test-renderer'); +const styled = require('../src').styled; + +it('renders tag with display name and class name', () => { + const Test = styled('h1')({ + name: 'TestComponent', + class: 'abcdefg', + }); + + expect(Test.displayName).toBe('TestComponent'); + expect(Test.__linaria.className).toBe('abcdefg'); + expect(Test.__linaria.extends).toBe('h1'); + + const tree = renderer.create(<Test>This is a test</Test>); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('renders component with display name and class name', () => { + const Custom = (props) => <div {...props} />; + + const Test = styled(Custom)({ + name: 'TestComponent', + class: 'abcdefg', + }); + + expect(Test.displayName).toBe('TestComponent'); + expect(Test.__linaria.className).toBe('abcdefg'); + expect(Test.__linaria.extends).toBe(Custom); + + const tree = renderer.create(<Test>This is a test</Test>); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('applies CSS variables in style prop', () => { + const Test = styled('div')({ + name: 'TestComponent', + class: 'abcdefg', + vars: { + foo: ['tomato'], + bar: [20, 'px'], + baz: [(props) => props.size, 'px'], + }, + }); + + const tree = renderer.create(<Test size={24}>This is a test</Test>); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('merges CSS variables with custom style prop', () => { + const Test = styled('div')({ + name: 'TestComponent', + class: 'abcdefg', + vars: { + foo: ['tomato'], + }, + }); + + const tree = renderer.create( + <Test style={{ bar: 'baz' }}>This is a test</Test> + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('supports extra className prop', () => { + const Test = styled('div')({ + name: 'TestComponent', + class: 'abcdefg', + }); + + const tree = renderer.create(<Test className="primary">This is a test</Test>); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('supports extra class prop', () => { + const Test = styled('div')({ + name: 'TestComponent', + class: 'abcdefg', + }); + + const tree = renderer.create(<Test class="primary">This is a test</Test>); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('replaces simple component with as prop', () => { + const Test = styled('button')({ + name: 'TestComponent', + class: 'abcdefg', + }); + + const tree = renderer.create( + <Test as="a" id="test" foo="bar"> + This is a test + </Test> + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('replaces custom component with as prop for primitive', () => { + const Custom = (props) => <div {...props} style={{ fontSize: 12 }} />; + + const Test = styled(Custom)({ + name: 'TestComponent', + class: 'abcdefg', + }); + + const tree = renderer.create( + <Test as="a" id="test" foo="bar"> + This is a test + </Test> + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('replaces primitive with as prop for custom component', () => { + const Custom = (props) => <div {...props} style={{ fontSize: 12 }} />; + + const Test = styled('div')({ + name: 'TestComponent', + class: 'abcdefg', + }); + + const tree = renderer.create( + <Test as={Custom} id="test" foo="bar"> + This is a test + </Test> + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('handles wrapping another styled component', () => { + const First = styled('div')({ + name: 'FirstComponent', + class: 'abcdefg', + }); + + const Second = styled(First)({ + name: 'SecondComponent', + class: 'hijklmn', + }); + + const tree = renderer.create(<Second>This is a test</Second>); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('forwards as prop when wrapping another styled component', () => { + const First = styled('div')({ + name: 'FirstComponent', + class: 'abcdefg', + }); + + const Second = styled(First)({ + name: 'SecondComponent', + class: 'hijklmn', + }); + + const tree = renderer.create(<Second as="a">This is a test</Second>); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('filters unknown html attributes for HTML tag', () => { + const Test = styled('div')({ + name: 'TestComponent', + class: 'abcdefg', + }); + + const tree = renderer.create( + <Test unknownAttribute="voila">This is a test</Test> + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('does not filter attributes for custom elements', () => { + const Test = styled('my-element')({ + name: 'TestComponent', + class: 'abcdefg', + }); + + const tree = renderer.create( + <Test unknownAttribute="voila">This is a test</Test> + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('does not filter attributes for components', () => { + const Custom = (props) => <div>{props.unknownAttribute}</div>; + + const Test = styled(Custom)({ + name: 'TestComponent', + class: 'abcdefg', + }); + + const tree = renderer.create( + <Test unknownAttribute="voila">This is a test</Test> + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('provides linaria component className for composition as last item in props.className', () => { + const Custom = (props) => { + const classnames = props.className.split(' '); + const linariaClassName = classnames[classnames.length - 1]; + const newClassNames = [ + props.className, + `${linariaClassName}--primary`, + `${linariaClassName}--accessibility`, + ].join(' '); + + return ( + <div className={newClassNames}> + original classname used for composition + </div> + ); + }; + + const Test = styled(Custom)({ + name: 'TestComponent', + class: 'abcdefg', + }); + + const tree = renderer.create( + <Test className="some-another-class">This is a test</Test> + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +it('throws when using as tag for template literal', () => { + expect( + () => + styled('div')` + color: blue; + ` + ).toThrow('Using the "styled" tag in runtime is not supported'); + + expect( + () => + styled.div` + color: blue; + ` + ).toThrow('Using the "styled" tag in runtime is not supported'); +}); diff --git a/@linaria/packages/react/babel.config.js b/@linaria/packages/react/babel.config.js new file mode 100644 index 0000000..c9ad680 --- /dev/null +++ b/@linaria/packages/react/babel.config.js @@ -0,0 +1,3 @@ +const config = require('../../babel.config'); + +module.exports = config; diff --git a/@linaria/packages/react/package.json b/@linaria/packages/react/package.json new file mode 100644 index 0000000..aaea46f --- /dev/null +++ b/@linaria/packages/react/package.json @@ -0,0 +1,53 @@ +{ + "name": "@linaria/react", + "version": "3.0.0-beta.11", + "publishConfig": { + "access": "public" + }, + "description": "Blazing fast zero-runtime CSS in JS library", + "sideEffects": false, + "main": "lib/index.js", + "module": "esm/index.js", + "types": "types/index.d.ts", + "files": [ + "types/", + "lib/", + "esm/" + ], + "license": "MIT", + "repository": "git@github.com:callstack/linaria.git", + "bugs": { + "url": "https://github.com/callstack/linaria/issues" + }, + "homepage": "https://github.com/callstack/linaria#readme", + "keywords": [ + "react", + "linaria", + "css", + "css-in-js", + "styled-components" + ], + "scripts": { + "build:lib": "cross-env NODE_ENV=legacy babel src --out-dir lib --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start", + "build:esm": "babel src --out-dir esm --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start", + "build": "yarn build:lib && yarn build:esm", + "build:declarations": "tsc --emitDeclarationOnly --outDir types", + "prepare": "yarn build && yarn build:declarations", + "test": "jest --config ../../jest.config.js --rootDir .", + "test:dts": "dtslint --localTs ../../node_modules/typescript/lib __dtslint__", + "typecheck": "tsc --noEmit --composite false", + "watch": "yarn build --watch" + }, + "devDependencies": { + "@types/react": ">=16", + "react": "^16.13.1", + "react-test-renderer": "^16.8.3" + }, + "dependencies": { + "@emotion/is-prop-valid": "^0.8.8", + "@linaria/core": "^3.0.0-beta.4" + }, + "peerDependencies": { + "react": ">=16" + } +} diff --git a/@linaria/packages/react/src/index.ts b/@linaria/packages/react/src/index.ts new file mode 100644 index 0000000..bece49d --- /dev/null +++ b/@linaria/packages/react/src/index.ts @@ -0,0 +1,2 @@ +export { default as styled } from './styled'; +export type { CSSProperties } from '@linaria/core'; diff --git a/@linaria/packages/react/src/styled.ts b/@linaria/packages/react/src/styled.ts new file mode 100644 index 0000000..1bcbe02 --- /dev/null +++ b/@linaria/packages/react/src/styled.ts @@ -0,0 +1,224 @@ +/** + * This file contains an runtime version of `styled` component. Responsibilities of the component are: + * - returns ReactElement based on HTML tag used with `styled` or custom React Component + * - injects classNames for the returned component + * - injects CSS variables used to define dynamic styles based on props + */ +import React from 'react'; +import validAttr from '@emotion/is-prop-valid'; +import { cx } from '@linaria/core'; +import type { CSSProperties, StyledMeta } from '@linaria/core'; + +export type NoInfer<A extends any> = [A][A extends any ? 0 : never]; + +type Options = { + name: string; + class: string; + vars?: { + [key: string]: [ + string | number | ((props: unknown) => string | number), + string | void + ]; + }; +}; + +// Workaround for rest operator +const restOp = ( + obj: Record<string, unknown>, + keysToExclude: string[] +): Record<string, unknown> => + Object.keys(obj) + .filter((prop) => keysToExclude.indexOf(prop) === -1) + .reduce((acc, curr) => { + acc[curr] = obj[curr]; + return acc; + }, {} as Record<string, unknown>); // rest operator workaround + +const warnIfInvalid = (value: any, componentName: string) => { + if (process.env.NODE_ENV !== 'production') { + if ( + typeof value === 'string' || + // eslint-disable-next-line no-self-compare + (typeof value === 'number' && isFinite(value)) + ) { + return; + } + + const stringified = + typeof value === 'object' ? JSON.stringify(value) : String(value); + + // eslint-disable-next-line no-console + console.warn( + `An interpolation evaluated to '${stringified}' in the component '${componentName}', which is probably a mistake. You should explicitly cast or transform the value to a string.` + ); + } +}; + +interface IProps { + className?: string; + style?: Record<string, string>; + [props: string]: unknown; +} + +// If styled wraps custom component, that component should have className property +function styled<TConstructor extends React.FunctionComponent<any>>( + tag: TConstructor extends React.FunctionComponent<infer T> + ? T extends { className?: string } + ? TConstructor + : never + : never +): ComponentStyledTag<TConstructor>; +function styled<T>( + tag: T extends { className?: string } ? React.ComponentType<T> : never +): ComponentStyledTag<T>; +function styled<TName extends keyof JSX.IntrinsicElements>( + tag: TName +): HtmlStyledTag<TName>; +function styled(tag: any): any { + return (options: Options) => { + if (process.env.NODE_ENV !== 'production') { + if (Array.isArray(options)) { + // We received a strings array since it's used as a tag + throw new Error( + 'Using the "styled" tag in runtime is not supported. Make sure you have set up the Babel plugin correctly. See https://github.com/callstack/linaria#setup' + ); + } + } + + const render = (props: any, ref: any) => { + const { as: component = tag, class: className } = props; + const rest = restOp(props, ['as', 'class']); + let filteredProps: IProps; + + // Check if it's an HTML tag and not a custom element + if (typeof component === 'string' && component.indexOf('-') === -1) { + filteredProps = {} as { [key: string]: any }; + + // eslint-disable-next-line guard-for-in + for (const key in rest) { + if (key === 'as' || validAttr(key)) { + // Don't pass through invalid attributes to HTML elements + filteredProps[key] = rest[key]; + } + } + } else { + filteredProps = rest; + } + + filteredProps.ref = ref; + filteredProps.className = cx( + filteredProps.className || className, + options.class + ); + + const { vars } = options; + + if (vars) { + const style: { [key: string]: string } = {}; + + // eslint-disable-next-line guard-for-in + for (const name in vars) { + const variable = vars[name]; + const result = variable[0]; + const unit = variable[1] || ''; + const value = typeof result === 'function' ? result(props) : result; + + warnIfInvalid(value, options.name); + + style[`--${name}`] = `${value}${unit}`; + } + + const ownStyle = filteredProps.style || {}; + const keys = Object.keys(ownStyle); + if (keys.length > 0) { + keys.forEach((key) => { + style[key] = ownStyle[key]; + }); + } + + filteredProps.style = style; + } + + if ((tag as any).__linaria && tag !== component) { + // If the underlying tag is a styled component, forward the `as` prop + // Otherwise the styles from the underlying component will be ignored + filteredProps.as = component; + + return React.createElement(tag, filteredProps); + } + return React.createElement(component, filteredProps); + }; + + const Result = React.forwardRef + ? React.forwardRef(render) + : // React.forwardRef won't available on older React versions and in Preact + // Fallback to a innerRef prop in that case + (props: any) => { + const rest = restOp(props, ['innerRef']); + return render(rest, props.innerRef); + }; + + (Result as any).displayName = options.name; + + // These properties will be read by the babel plugin for interpolation + (Result as any).__linaria = { + className: options.class, + extends: tag, + }; + + return Result; + }; +} + +type StyledComponent<T> = StyledMeta & + (T extends React.FunctionComponent<any> + ? T + : React.FunctionComponent<T & { as?: React.ElementType }>); + +type StaticPlaceholder = string | number | CSSProperties | StyledMeta; + +type HtmlStyledTag<TName extends keyof JSX.IntrinsicElements> = < + TAdditionalProps = {} +>( + strings: TemplateStringsArray, + ...exprs: Array< + | StaticPlaceholder + | (( + // Without Omit here TS tries to infer TAdditionalProps + // from a component passed for interpolation + props: JSX.IntrinsicElements[TName] & Omit<TAdditionalProps, never> + ) => string | number) + > +) => StyledComponent<JSX.IntrinsicElements[TName] & TAdditionalProps>; + +type ComponentStyledTag<T> = < + OwnProps = {}, + TrgProps = T extends React.FunctionComponent<infer TProps> ? TProps : T +>( + strings: TemplateStringsArray, + // Expressions can contain functions only if wrapped component has style property + ...exprs: TrgProps extends { style?: React.CSSProperties } + ? Array< + | StaticPlaceholder + | ((props: NoInfer<OwnProps & TrgProps>) => string | number) + > + : StaticPlaceholder[] +) => keyof OwnProps extends never + ? T extends React.FunctionComponent<any> + ? StyledMeta & T + : StyledComponent<TrgProps> + : StyledComponent<OwnProps & TrgProps>; + +type StyledJSXIntrinsics = { + readonly [P in keyof JSX.IntrinsicElements]: HtmlStyledTag<P>; +}; + +export type Styled = typeof styled & StyledJSXIntrinsics; + +export default (process.env.NODE_ENV !== 'production' + ? new Proxy(styled, { + get(o, prop: keyof JSX.IntrinsicElements) { + return o(prop); + }, + }) + : styled) as Styled; diff --git a/@linaria/packages/react/tsconfig.json b/@linaria/packages/react/tsconfig.json new file mode 100644 index 0000000..61b3298 --- /dev/null +++ b/@linaria/packages/react/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "paths": {}, + "rootDir": "src/" + }, + "references": [{ "path": "../core" }] +} |