summaryrefslogtreecommitdiff
path: root/@linaria/packages/react
diff options
context:
space:
mode:
Diffstat (limited to '@linaria/packages/react')
-rw-r--r--@linaria/packages/react/CHANGELOG.md46
-rw-r--r--@linaria/packages/react/README.md35
-rw-r--r--@linaria/packages/react/__dtslint__/index.d.ts2
-rw-r--r--@linaria/packages/react/__dtslint__/styled.ts135
-rw-r--r--@linaria/packages/react/__dtslint__/tsconfig.json14
-rw-r--r--@linaria/packages/react/__tests__/__snapshots__/styled.test.js.snap143
-rw-r--r--@linaria/packages/react/__tests__/detect-core-js.test.js35
-rw-r--r--@linaria/packages/react/__tests__/styled.test.js256
-rw-r--r--@linaria/packages/react/babel.config.js3
-rw-r--r--@linaria/packages/react/package.json53
-rw-r--r--@linaria/packages/react/src/index.ts2
-rw-r--r--@linaria/packages/react/src/styled.ts224
-rw-r--r--@linaria/packages/react/tsconfig.json8
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" }]
+}