summaryrefslogtreecommitdiff
path: root/@linaria/packages/extractor
diff options
context:
space:
mode:
Diffstat (limited to '@linaria/packages/extractor')
-rw-r--r--@linaria/packages/extractor/CHANGELOG.md51
-rw-r--r--@linaria/packages/extractor/README.md35
-rw-r--r--@linaria/packages/extractor/__tests__/__snapshots__/extractor.test.ts.snap1126
-rw-r--r--@linaria/packages/extractor/__tests__/extractor.test.ts5
-rw-r--r--@linaria/packages/extractor/babel.config.js3
-rw-r--r--@linaria/packages/extractor/package.json51
-rw-r--r--@linaria/packages/extractor/src/RequirementsResolver.ts212
-rw-r--r--@linaria/packages/extractor/src/index.ts144
-rw-r--r--@linaria/packages/extractor/tsconfig.json5
9 files changed, 1632 insertions, 0 deletions
diff --git a/@linaria/packages/extractor/CHANGELOG.md b/@linaria/packages/extractor/CHANGELOG.md
new file mode 100644
index 0000000..99b988e
--- /dev/null
+++ b/@linaria/packages/extractor/CHANGELOG.md
@@ -0,0 +1,51 @@
+# 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.8](https://github.com/callstack/linaria/compare/v3.0.0-beta.7...v3.0.0-beta.8) (2021-07-18)
+
+**Note:** Version bump only for package @linaria/extractor
+
+
+
+
+
+# [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/extractor
+
+
+
+
+
+# [3.0.0-beta.5](https://github.com/callstack/linaria/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2021-05-31)
+
+
+### Bug Fixes
+
+* **shaker:** typescript enums support ([#761](https://github.com/callstack/linaria/issues/761)) ([#764](https://github.com/callstack/linaria/issues/764)) ([6907e22](https://github.com/callstack/linaria/commit/6907e2280a2ab8ee014b5d02b1169714ccac9d66))
+
+
+
+
+
+# [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/extractor
+
+
+
+
+
+# [3.0.0-beta.3](https://github.com/callstack/linaria/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2021-04-20)
+
+**Note:** Version bump only for package @linaria/extractor
+
+
+
+
+
+# [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/extractor
diff --git a/@linaria/packages/extractor/README.md b/@linaria/packages/extractor/README.md
new file mode 100644
index 0000000..0d75b37
--- /dev/null
+++ b/@linaria/packages/extractor/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/extractor/__tests__/__snapshots__/extractor.test.ts.snap b/@linaria/packages/extractor/__tests__/__snapshots__/extractor.test.ts.snap
new file mode 100644
index 0000000..c3c9582
--- /dev/null
+++ b/@linaria/packages/extractor/__tests__/__snapshots__/extractor.test.ts.snap
@@ -0,0 +1,1126 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`extractor derives display name from filename 1`] = `
+"import { styled } from '@linaria/react';
+export default /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"FancyName0\\",
+ class: \\"FancyName0_f1tjvuuv\\"
+});"
+`;
+
+exports[`extractor derives display name from filename 2`] = `
+
+CSS:
+
+.FancyName0_f1tjvuuv {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor derives display name from parent folder name 1`] = `
+"import { styled } from '@linaria/react';
+export default /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"FancyName0\\",
+ class: \\"FancyName0_fud4l0y\\"
+});"
+`;
+
+exports[`extractor derives display name from parent folder name 2`] = `
+
+CSS:
+
+.FancyName0_fud4l0y {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor does not strip instanbul coverage sequences 1`] = `
+"var cov_2dr9r1nt95 = function () {
+ var path = \\"/home/user/project/file.js\\";
+ var hash = \\"23f56bbaeb2ebcd213f41374d3b2af1bce287bd3\\";
+ var global = new Function(\\"return this\\")();
+ var gcv = \\"__coverage__\\";
+ var coverageData = {
+ path: \\"/home/user/project/file.js\\",
+ statementMap: {
+ \\"0\\": {
+ start: {
+ line: 3,
+ column: 10
+ },
+ end: {
+ line: 3,
+ column: 12
+ }
+ },
+ \\"1\\": {
+ start: {
+ line: 5,
+ column: 21
+ },
+ end: {
+ line: 7,
+ column: 1
+ }
+ }
+ },
+ fnMap: {},
+ branchMap: {},
+ s: {
+ \\"0\\": 0,
+ \\"1\\": 0
+ },
+ f: {},
+ b: {},
+ _coverageSchema: \\"43e27e138ebf9cfc5966b082cf9a028302ed4184\\",
+ hash: \\"23f56bbaeb2ebcd213f41374d3b2af1bce287bd3\\"
+ };
+ var coverage = global[gcv] || (global[gcv] = {});
+
+ if (coverage[path] && coverage[path].hash === hash) {
+ return coverage[path];
+ }
+
+ return coverage[path] = coverageData;
+}();
+
+import { styled } from '@linaria/react';
+const a = (cov_2dr9r1nt95.s[0]++, 42);
+export const Title = (cov_2dr9r1nt95.s[1]++, /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_tow9xsn\\"
+}));"
+`;
+
+exports[`extractor does not strip instanbul coverage sequences 2`] = `
+
+CSS:
+
+.Title_tow9xsn {
+ height: 42px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor evaluates babel helpers 1`] = `
+"import { styled } from '@linaria/react';
+
+function copyAndExtend(a, b) {
+ return { ...a,
+ ...b
+ };
+}
+
+const obj = copyAndExtend({
+ a: 1
+}, {
+ a: 2
+});
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`extractor evaluates babel helpers 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "2"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor evaluates complex styles with functions and nested selectors 1`] = `
+"import { css } from '@linaria/core';
+export const bareIconClass = \\"bareIconClass_b1t92lw9\\";
+
+const getSizeStyles = fs => ({
+ [\`&.\${bareIconClass}\`]: {
+ fontSize: fs * 1.5
+ }
+});
+
+export const SIZES = {
+ XS: \\"XS_x1xjmq2i\\"
+};"
+`;
+
+exports[`extractor evaluates complex styles with functions and nested selectors 2`] = `
+
+CSS:
+
+.bareIconClass_b1t92lw9 {}
+.XS_x1xjmq2i {&.bareIconClass_b1t92lw9 { font-size: 16.5px; }}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor evaluates component interpolations 1`] = `
+"const {
+ styled
+} = require('@linaria/react');
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});
+export const Paragraph = /*#__PURE__*/styled(\\"p\\")({
+ name: \\"Paragraph\\",
+ class: \\"Paragraph_p1xjmq2i\\"
+});"
+`;
+
+exports[`extractor evaluates component interpolations 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ color: red;
+}
+.Paragraph_p1xjmq2i {
+ .Title_t1t92lw9 {
+ color: blue;
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor evaluates dependencies with sequence expression 1`] = `
+"import { styled } from '@linaria/react';
+const color = (external, () => 'blue');
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\",
+ vars: {
+ \\"t1t92lw9-0\\": [color]
+ }
+});"
+`;
+
+exports[`extractor evaluates dependencies with sequence expression 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ color: var(--t1t92lw9-0);
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor evaluates expressions with dependencies 1`] = `
+"import { styled } from '@linaria/react';
+import slugify from '@linaria/babel-preset/__fixtures__/slugify';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`extractor evaluates expressions with dependencies 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "6og6jy"
+ }
+}
+
+Dependencies: @babel/runtime/helpers/interopRequireDefault, @linaria/babel-preset/__fixtures__/slugify
+
+`;
+
+exports[`extractor evaluates expressions with expressions depending on shared dependency 1`] = `
+"import { styled } from '@linaria/react';
+
+const slugify = require('@linaria/babel-preset/__fixtures__/slugify').default;
+
+const boo = t => slugify(t) + 'boo';
+
+const bar = t => slugify(t) + 'bar';
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`extractor evaluates expressions with expressions depending on shared dependency 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "6og6jyboo6og6jybar"
+ }
+}
+
+Dependencies: @linaria/babel-preset/__fixtures__/slugify
+
+`;
+
+exports[`extractor evaluates functions with nested identifiers 1`] = `
+"import { styled } from '@linaria/react';
+const objects = {
+ key: {
+ fontSize: 12
+ }
+};
+
+const foo = k => {
+ const obj = objects[k];
+ return obj;
+};
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`extractor evaluates functions with nested identifiers 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ font-size: 12px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor evaluates identifier in scope 1`] = `
+"import { styled } from '@linaria/react';
+const answer = 42;
+
+const foo = () => answer;
+
+const days = foo() + ' days';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`extractor evaluates identifier in scope 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "42 days"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor evaluates imported typescript enums 1`] = `
+"import { styled } from '@linaria/react';
+import { Colors } from '@linaria/babel-preset/__fixtures__/enums';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_taxxqrn\\"
+});"
+`;
+
+exports[`extractor evaluates imported typescript enums 2`] = `
+
+CSS:
+
+.Title_taxxqrn {
+ color: #27509A;
+}
+
+Dependencies: @linaria/babel-preset/__fixtures__/enums
+
+`;
+
+exports[`extractor evaluates interpolations with sequence expression 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\",
+ vars: {
+ \\"t1t92lw9-0\\": [(external, () => \\"blue\\")]
+ }
+});"
+`;
+
+exports[`extractor evaluates interpolations with sequence expression 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ color: var(--t1t92lw9-0);
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor evaluates local expressions 1`] = `
+"import { styled } from '@linaria/react';
+const answer = 42;
+
+const foo = () => answer;
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`extractor evaluates local expressions 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "42 days"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor evaluates multiple expressions with shared dependency 1`] = `
+"import { styled } from '@linaria/react';
+
+const slugify = require('@linaria/babel-preset/__fixtures__/slugify').default;
+
+const boo = t => slugify(t) + 'boo';
+
+const bar = t => slugify(t) + 'bar';
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`extractor evaluates multiple expressions with shared dependency 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "6og6jyboo"
+ content: "6og6jybar"
+ }
+}
+
+Dependencies: @linaria/babel-preset/__fixtures__/slugify
+
+`;
+
+exports[`extractor generates stable class names 1`] = `
+"import { styled } from '@linaria/react';
+export const T1 = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"T1\\",
+ class: \\"T1_t16d1w5s\\"
+});
+export const T2 = /*#__PURE__*/styled(\\"h2\\")({
+ name: \\"T2\\",
+ class: \\"T2_t18j681n\\"
+});
+export const T3 = /*#__PURE__*/styled(\\"h3\\")({
+ name: \\"T3\\",
+ class: \\"T3_t16ajafo\\"
+});
+export default /*#__PURE__*/styled(\\"p\\")({
+ name: \\"components-library3\\",
+ class: \\"components-library3_c16bmp8w\\"
+});"
+`;
+
+exports[`extractor generates stable class names 2`] = `
+
+CSS:
+
+.T1_t16d1w5s {
+ background: #111;
+}
+.T2_t18j681n {
+ background: #222;
+}
+.T3_t16ajafo {
+ .T2_t18j681n {
+ background: #333;
+ }
+}
+.components-library3_c16bmp8w {
+ background: #333;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor handles complex component 1`] = `
+"// Dead code in this file should be ignored
+import deadDep from 'unknown-dependency';
+import { styled } from '@linaria/react';
+export const deadValue = deadDep();
+const objects = {
+ font: {
+ fontSize: 12
+ },
+ box: {
+ border: '1px solid red'
+ }
+};
+
+const foo = k => {
+ const {
+ [k]: obj
+ } = objects;
+ return obj;
+};
+
+objects.font.fontWeight = 'bold';
+export const whiteColor = '#fff';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t16vg7lb\\"
+});"
+`;
+
+exports[`extractor handles complex component 2`] = `
+
+CSS:
+
+.Title_t16vg7lb {
+ font-size: 12px;
+ border: 1px solid red;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor handles escapes properly 1`] = `
+"import { styled } from '@linaria/react';
+const selectors = ['a', 'b'];
+export const Block = /*#__PURE__*/styled(\\"div\\")({
+ name: \\"Block\\",
+ class: \\"Block_b11ngyv0\\"
+});"
+`;
+
+exports[`extractor handles escapes properly 2`] = `
+
+CSS:
+
+.Block_b11ngyv0 {
+ a { content: "\\u000A"; } b { content: "\\u000A"; };
+}
+
+Dependencies: @babel/runtime/helpers/interopRequireDefault, @babel/runtime/helpers/taggedTemplateLiteral
+
+`;
+
+exports[`extractor handles indirect wrapping another styled component 1`] = `
+"const {
+ styled
+} = require('@linaria/react');
+
+const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});
+
+const hoc = Cmp => Cmp;
+
+export const CustomTitle = /*#__PURE__*/styled(hoc(Title))({
+ name: \\"CustomTitle\\",
+ class: \\"CustomTitle_c1xjmq2i\\"
+});"
+`;
+
+exports[`extractor handles indirect wrapping another styled component 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ color: red;
+}
+.CustomTitle_c1xjmq2i {
+ font-size: 24px;
+ color: blue;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor handles wrapping another styled component 1`] = `
+"const {
+ css
+} = require('..');
+
+const {
+ styled
+} = require('@linaria/react');
+
+const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});
+export const BlueTitle = /*#__PURE__*/styled(Title)({
+ name: \\"BlueTitle\\",
+ class: \\"BlueTitle_b1xjmq2i\\"
+});
+export const GreenTitle = /*#__PURE__*/styled(BlueTitle)({
+ name: \\"GreenTitle\\",
+ class: \\"GreenTitle_g2qjq78\\"
+});"
+`;
+
+exports[`extractor handles wrapping another styled component 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ color: red;
+}
+.BlueTitle_b1xjmq2i.Title_t1t92lw9 {
+ font-size: 24px;
+ color: blue;
+}
+.GreenTitle_g2qjq78.BlueTitle_b1xjmq2i.Title_t1t92lw9 {
+ color: green;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor hoistable identifiers 1`] = `
+"import { styled } from '@linaria/react';
+{
+ var days = 42;
+}
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`extractor hoistable identifiers 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "42"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor ignores external expressions 1`] = `
+"import { styled } from '@linaria/react';
+
+const generate = props => props.content;
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\",
+ vars: {
+ \\"t1t92lw9-0\\": [generate]
+ }
+});"
+`;
+
+exports[`extractor ignores external expressions 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "var(--t1t92lw9-0)"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor ignores inline arrow function expressions 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\",
+ vars: {
+ \\"t1t92lw9-0\\": [props => props.content]
+ }
+});"
+`;
+
+exports[`extractor ignores inline arrow function expressions 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "var(--t1t92lw9-0)"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor ignores inline vanilla function expressions 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\",
+ vars: {
+ \\"t1t92lw9-0\\": [function (props) {
+ return props.content;
+ }]
+ }
+});"
+`;
+
+exports[`extractor ignores inline vanilla function expressions 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "var(--t1t92lw9-0)"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor inlines array styles as CSS string 1`] = `
+"import { styled } from '@linaria/react';
+
+const fill = (top = 0, left = 0, right = 0, bottom = 0) => [{
+ position: 'absolute'
+}, {
+ top,
+ right,
+ bottom,
+ left
+}];
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`extractor inlines array styles as CSS string 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ position: absolute; top: 0; right: 0; bottom: 0; left: 0;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor inlines object styles as CSS string 1`] = `
+"import { styled } from '@linaria/react';
+
+const fill = (top = 0, left = 0, right = 0, bottom = 0) => ({
+ position: 'absolute',
+ top,
+ right,
+ bottom,
+ left
+});
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`extractor inlines object styles as CSS string 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ position: absolute; top: 0; right: 0; bottom: 0; left: 0;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor it should not throw location error for hoisted identifier 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+
+const size = () => 5;
+
+var _ref = size();
+
+export default function Component() {
+ const color = _ref;
+ return \\"source0_s1t92lw9\\";
+}"
+`;
+
+exports[`extractor it should not throw location error for hoisted identifier 2`] = `
+
+CSS:
+
+.source0_s1t92lw9 {opacity:5;}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor non-hoistable identifiers 1`] = `
+"<<DIRNAME>>/source.js: An error occurred when evaluating the expression:
+
+ > days is not defined.
+
+ Make sure you are not using a browser or Node specific API and all the variables are available in static context.
+ Linaria have to extract pieces of your code to resolve the interpolated values.
+ Defining styled component or class will not work inside:
+ - function,
+ - class,
+ - method,
+ - loop,
+ because it cannot be statically determined in which context you use them.
+ That's why some variables may be not defined during evaluation.
+
+ 7 | export const Title = styled.h1\`
+ 8 | &:before {
+> 9 | content: \\"\${days}\\"
+ | ^^^^
+ 10 | }
+ 11 | \`;"
+`;
+
+exports[`extractor should handle shadowed identifier inside components 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+const color = 'red';
+var _ref = 'blue';
+var _ref2 = {
+ color: _ref
+};
+export default function Component() {
+ const color = _ref;
+ const val = _ref2;
+ return React.createElement('div', {
+ className: \\"className_c1t92lw9\\"
+ });
+}"
+`;
+
+exports[`extractor should handle shadowed identifier inside components 2`] = `
+
+CSS:
+
+.className_c1t92lw9 {background-color:blue;}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor should process \`css\` calls inside components 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+export function Component() {
+ const opacity = 0.2;
+ const className = \\"className_c1t92lw9\\";
+ return React.createElement(\\"div\\", {
+ className
+ });
+}"
+`;
+
+exports[`extractor should process \`css\` calls inside components 2`] = `
+
+CSS:
+
+.className_c1t92lw9 {
+ opacity: 0.2;
+ }
+
+Dependencies: NA
+
+`;
+
+exports[`extractor should process \`css\` calls with complex interpolation inside components 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+import externalDep from '@linaria/babel-preset/__fixtures__/sample-script';
+const globalObj = {
+ opacity: 0.5
+};
+var _ref = externalDep;
+var _ref2 = {
+ value: 0.2,
+ cell: \\"cell_c1t92lw9\\"
+};
+var _ref3 = _ref2;
+export function Component() {
+ const classes = _ref2;
+ const classes2 = _ref3;
+ const referencedExternalDep = _ref;
+ const className = \\"className_c1xjmq2i\\";
+ return React.createElement(\\"div\\", {
+ className
+ });
+}"
+`;
+
+exports[`extractor should process \`css\` calls with complex interpolation inside components 2`] = `
+
+CSS:
+
+.cell_c1t92lw9 {
+ opacity: 0;
+ }
+.className_c1xjmq2i {
+ opacity: 0.5;
+ font-size: 42
+ font-size: 42
+
+ &:hover .cell_c1t92lw9 {
+ opacity: 0.2;
+ }
+ }
+
+Dependencies: @babel/runtime/helpers/interopRequireDefault, @linaria/babel-preset/__fixtures__/sample-script
+
+`;
+
+exports[`extractor should process \`styled\` calls inside components 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+export function Component() {
+ const opacity = 0.2;
+ const MyComponent = styled.h1\`
+ opacity: \${opacity};
+ \`;
+ return React.createElement(MyComponent);
+}"
+`;
+
+exports[`extractor should process \`styled\` calls inside components 2`] = `Object {}`;
+
+exports[`extractor should process \`styled\` calls with complex interpolation inside components 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+const globalObj = {
+ opacity: 0.5
+};
+const Styled1 = styled.p\`
+ opacity: \${globalObj.opacity}
+\`;
+export function Component() {
+ const classes = {
+ value: 0.2,
+ cell: \\"cell_c1t92lw9\\"
+ };
+ const classes2 = classes;
+ const MyComponent = styled\`
+ opacity: \${globalObj.opacity};
+
+ &:hover .\${classes2.cell} {
+ opacity: \${classes.value};
+ }
+ \${Styled1} {
+ font-size: 1;
+ }
+ \`;
+ return React.createElement(MyComponent);
+}"
+`;
+
+exports[`extractor should process \`styled\` calls with complex interpolation inside components 2`] = `
+
+CSS:
+
+.cell_c1t92lw9 {
+ opacity: 0;
+ }
+
+Dependencies: NA
+
+`;
+
+exports[`extractor should work with String and Number object 1`] = `
+"import { css } from '@linaria/core';
+export const style = \\"style_s1t92lw9\\";"
+`;
+
+exports[`extractor should work with String and Number object 2`] = `
+
+CSS:
+
+.style_s1t92lw9 {
+ width: 100%;
+ opacity: 0.75;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor should work with generated classnames as selectors 1`] = `
+"import { css } from \\"@linaria/core\\";
+export const text = \\"text_t1t92lw9\\";
+export const square = \\"square_s1xjmq2i\\";"
+`;
+
+exports[`extractor should work with generated classnames as selectors 2`] = `
+
+CSS:
+
+.text_t1t92lw9 {}
+.square_s1xjmq2i {
+ .text_t1t92lw9 {
+ color: red;
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`extractor simplifies react components 1`] = `
+"import React from 'react';
+import { styled } from '@linaria/react';
+import constant from './broken-dependency';
+
+const FuncComponent = props => <div>{props.children + constant}</div>;
+
+class ClassComponent extends React.PureComponent {
+ method() {
+ return constant;
+ }
+
+ render() {
+ return <div>{props.children + constant}</div>;
+ }
+
+}
+
+export const StyledFunc = /*#__PURE__*/styled(FuncComponent)({
+ name: \\"StyledFunc\\",
+ class: \\"StyledFunc_s1t92lw9\\"
+});
+export const StyledClass = /*#__PURE__*/styled(ClassComponent)({
+ name: \\"StyledClass\\",
+ class: \\"StyledClass_s1xjmq2i\\"
+});"
+`;
+
+exports[`extractor simplifies react components 2`] = `
+
+CSS:
+
+.StyledFunc_s1t92lw9 {
+ color: red;
+}
+.StyledClass_s1xjmq2i {
+ color: blue;
+}
+
+Dependencies: @babel/runtime/helpers/interopRequireDefault, react
+
+`;
+
+exports[`extractor throws codeframe error when evaluation fails 1`] = `
+"<<DIRNAME>>/source.js: An error occurred when evaluating the expression:
+
+ > This will fail.
+
+ Make sure you are not using a browser or Node specific API and all the variables are available in static context.
+ Linaria have to extract pieces of your code to resolve the interpolated values.
+ Defining styled component or class will not work inside:
+ - function,
+ - class,
+ - method,
+ - loop,
+ because it cannot be statically determined in which context you use them.
+ That's why some variables may be not defined during evaluation.
+
+ 4 |
+ 5 | export const Title = styled.h1\`
+> 6 | font-size: \${foo()}px;
+ | ^^^^^
+ 7 | \`;"
+`;
+
+exports[`extractor throws if couldn't determine a display name 1`] = `
+"<<DIRNAME>>/.js: Couldn't determine a name for the component. Ensure that it's either:
+- Assigned to a variable
+- Is an object property
+- Is a prop in a JSX element
+
+ 1 | import { styled } from '@linaria/react';
+ 2 |
+> 3 | export default styled.h1\`
+ | ^
+ 4 | font-size: 14px;
+ 5 | \`;"
+`;
+
+exports[`extractor throws when interpolation evaluates to NaN 1`] = `
+"<<DIRNAME>>/source.js: The expression evaluated to 'NaN', which is probably a mistake. If you want it to be inserted into CSS, explicitly cast or transform the value to a string, e.g. - 'String(height)'.
+ 4 |
+ 5 | export const Title = styled.h1\`
+> 6 | height: \${height}px;
+ | ^^^^^^
+ 7 | \`;"
+`;
+
+exports[`extractor throws when interpolation evaluates to null 1`] = `
+"<<DIRNAME>>/source.js: The expression evaluated to 'null', which is probably a mistake. If you want it to be inserted into CSS, explicitly cast or transform the value to a string, e.g. - 'String(color)'.
+ 4 |
+ 5 | export const Title = styled.h1\`
+> 6 | color: \${color};
+ | ^^^^^
+ 7 | \`;"
+`;
+
+exports[`extractor throws when interpolation evaluates to undefined 1`] = `
+"<<DIRNAME>>/source.js: The expression evaluated to 'undefined', which is probably a mistake. If you want it to be inserted into CSS, explicitly cast or transform the value to a string, e.g. - 'String(fontSize)'.
+ 4 |
+ 5 | export const Title = styled.h1\`
+> 6 | font-size: \${fontSize};
+ | ^^^^^^^^
+ 7 | \`;"
+`;
diff --git a/@linaria/packages/extractor/__tests__/extractor.test.ts b/@linaria/packages/extractor/__tests__/extractor.test.ts
new file mode 100644
index 0000000..0883339
--- /dev/null
+++ b/@linaria/packages/extractor/__tests__/extractor.test.ts
@@ -0,0 +1,5 @@
+import { run } from '@linaria/babel-preset/__utils__/strategy-tester';
+
+describe('extractor', () => {
+ run(__dirname, require('../src').default);
+});
diff --git a/@linaria/packages/extractor/babel.config.js b/@linaria/packages/extractor/babel.config.js
new file mode 100644
index 0000000..c9ad680
--- /dev/null
+++ b/@linaria/packages/extractor/babel.config.js
@@ -0,0 +1,3 @@
+const config = require('../../babel.config');
+
+module.exports = config;
diff --git a/@linaria/packages/extractor/package.json b/@linaria/packages/extractor/package.json
new file mode 100644
index 0000000..68a4545
--- /dev/null
+++ b/@linaria/packages/extractor/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@linaria/extractor",
+ "version": "3.0.0-beta.8",
+ "publishConfig": {
+ "access": "public"
+ },
+ "description": "Blazing fast zero-runtime CSS in JS library",
+ "main": "lib/index.js",
+ "module": "esm/index.js",
+ "types": "types",
+ "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",
+ "babel-plugin",
+ "babel"
+ ],
+ "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 .",
+ "typecheck": "tsc --noEmit --composite false",
+ "watch": "yarn build --watch"
+ },
+ "dependencies": {
+ "@babel/generator": ">=7",
+ "@babel/plugin-transform-runtime": ">=7",
+ "@babel/plugin-transform-template-literals": ">=7",
+ "@linaria/babel-preset": "^3.0.0-beta.7",
+ "@linaria/preeval": "^3.0.0-beta.8"
+ },
+ "peerDependencies": {
+ "@babel/core": ">=7"
+ }
+}
diff --git a/@linaria/packages/extractor/src/RequirementsResolver.ts b/@linaria/packages/extractor/src/RequirementsResolver.ts
new file mode 100644
index 0000000..019177b
--- /dev/null
+++ b/@linaria/packages/extractor/src/RequirementsResolver.ts
@@ -0,0 +1,212 @@
+/**
+ * This file is used to extract statements required to evaluate dependencies.
+ * Starting from the exports.__linariaPreval passed as argument to static method on class RequirementsResolver,
+ * it recursively extracts paths that contains identifiers that are needed to evaluate the dependency.
+ */
+
+import { types as t } from '@babel/core';
+import type {
+ Identifier,
+ Node,
+ Statement,
+ VariableDeclarator,
+} from '@babel/types';
+import type { Binding, NodePath } from '@babel/traverse';
+
+type Requirement = {
+ result: Statement;
+ path: NodePath<Node>;
+ requirements: Set<NodePath>;
+};
+
+export default class RequirementsResolver {
+ public static resolve(path: NodePath<Node> | NodePath<Node>[]): Statement[] {
+ const resolver = new RequirementsResolver();
+ if (Array.isArray(path)) {
+ path.forEach((p) => this.resolve(p));
+ } else {
+ resolver.resolve(path);
+ }
+
+ return resolver.statements;
+ }
+
+ private requirements: Requirement[] = [];
+
+ /**
+ * Checks that specified node or one of its ancestors is already added
+ */
+ private isAdded(path: NodePath<Node>): boolean {
+ if (this.requirements.some((req) => req.path === path)) {
+ return true;
+ }
+
+ if (path.parentPath) {
+ return this.isAdded(path.parentPath);
+ }
+
+ return false;
+ }
+
+ /**
+ * Makes a declaration statement, finds dependencies
+ * and adds all of it to the list of requirements.
+ */
+ private resolveBinding(binding: Binding) {
+ let result: Statement;
+ const startPosition = binding.path.node.start;
+
+ switch (binding.kind) {
+ case 'module':
+ if (
+ binding.path.isImportSpecifier() &&
+ binding.path.parentPath.isImportDeclaration()
+ ) {
+ result = t.importDeclaration(
+ [binding.path.node],
+ binding.path.parentPath.node.source
+ );
+ } else {
+ result = binding.path.parentPath.node as Statement;
+ }
+ break;
+ case 'const':
+ case 'let':
+ case 'var': {
+ let decl = (binding.path as NodePath<VariableDeclarator>).node;
+ if (
+ binding.path.isVariableDeclarator() &&
+ t.isSequenceExpression(binding.path.node.init)
+ ) {
+ // Replace SequenceExpressions (expr1, expr2, expr3, ...) with the last one
+ decl = t.variableDeclarator(
+ binding.path.node.id,
+ binding.path.node.init.expressions[
+ binding.path.node.init.expressions.length - 1
+ ]
+ );
+ }
+
+ result = t.variableDeclaration(binding.kind, [decl]);
+ break;
+ }
+ default:
+ result = binding.path.node as Statement;
+ break;
+ }
+ // result may be newly created node that not have start/end/loc info
+ // which is needed to sort statements
+ result.start = startPosition;
+
+ const req: Requirement = {
+ result,
+ path: binding.path,
+ requirements: new Set(),
+ };
+
+ this.requirements.push(req);
+
+ req.requirements = this.resolve(binding.path);
+ }
+
+ /**
+ * Checks that a specified identifier has a binding and tries to resolve it
+ * @return `Binding` or null if there is no binding, or it is already added, or it has useless type
+ */
+ private resolveIdentifier(path: NodePath<Identifier>): Binding | null {
+ const binding = path.scope.getBinding(path.node.name);
+
+ if (
+ path.isReferenced() &&
+ binding &&
+ !this.isAdded(binding.path) &&
+ // @ts-ignore binding.kind can be param
+ binding.kind !== 'param'
+ ) {
+ this.resolveBinding(binding);
+ return binding;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds all identifiers in a specified path, finds all related bindings
+ * and recursively calls `resolve` for each of them.
+ * @return `Set` with related bindings
+ */
+ private resolve(path: NodePath<Node>): Set<NodePath> {
+ const set = new Set<NodePath>();
+ if (path.isIdentifier()) {
+ const binding = this.resolveIdentifier(path);
+ if (binding !== null) {
+ set.add(binding.path);
+ }
+
+ return set;
+ }
+
+ path.traverse({
+ Identifier: (p) => {
+ const binding = this.resolveIdentifier(p);
+ if (binding !== null) {
+ set.add(binding.path);
+ }
+ },
+ });
+
+ return set;
+ }
+
+ /**
+ * Returns sorted list of required statements
+ */
+ private get statements(): Statement[] {
+ const statements: Statement[] = [];
+ let requirements = this.requirements;
+ while (requirements.length > 0) {
+ // On each step, we add to the result list only that statements
+ // which don't have any dependencies (`zeroDeps`)
+ const [zeroDeps, rest] = requirements.reduce(
+ (acc, req) => {
+ if (req.requirements.size === 0) {
+ acc[0].push(req);
+ } else {
+ acc[1].push(req);
+ }
+
+ return acc;
+ },
+ [[], []] as [Requirement[], Requirement[]]
+ );
+
+ if (zeroDeps.length === 0) {
+ // That means that we are in the endless loop.
+ // I don't know how it's possible, but if it's ever happened, we at least would be notified.
+ throw new Error('Circular dependency');
+ }
+
+ statements.push(...zeroDeps.map((req) => req.result));
+ // Let's remove already added statements from the requirements of the rest of the list.
+ requirements = rest.map((req) => {
+ const reqs = new Set(req.requirements);
+ zeroDeps.forEach((r) => reqs.delete(r.path));
+ return {
+ ...req,
+ requirements: reqs,
+ };
+ });
+ }
+
+ // preserve original statements order, but reversed
+ statements.sort((a, b) => {
+ if (a.start && b.start) {
+ return b.start - a.start;
+ } else {
+ return 0;
+ }
+ });
+
+ return statements;
+ }
+}
diff --git a/@linaria/packages/extractor/src/index.ts b/@linaria/packages/extractor/src/index.ts
new file mode 100644
index 0000000..385b748
--- /dev/null
+++ b/@linaria/packages/extractor/src/index.ts
@@ -0,0 +1,144 @@
+/**
+ * This file is a main file of extractor evaluation strategy.
+ * It finds __linariaPreval statements starting from the end of the program and
+ * invoke RequirementsResolver to get parts of code that needs to be executed in order to evaluate the dependency.
+ */
+
+import { traverse, types as t } from '@babel/core';
+import type {
+ ExpressionStatement,
+ MemberExpression,
+ Program,
+ SequenceExpression,
+} from '@babel/types';
+import { parseSync, transformSync } from '@babel/core';
+import type { NodePath } from '@babel/traverse';
+import generator from '@babel/generator';
+
+import type { Evaluator } from '@linaria/babel-preset';
+import { buildOptions } from '@linaria/babel-preset';
+import RequirementsResolver from './RequirementsResolver';
+
+function isMemberExpression(
+ path: NodePath<any> | NodePath<any>[]
+): path is NodePath<MemberExpression> {
+ return !Array.isArray(path) && path.isMemberExpression();
+}
+
+// Checks that passed node is `exports.__linariaPreval = /* something */`
+function isLinariaPrevalExport(
+ path: NodePath<any>
+): path is NodePath<ExpressionStatement> {
+ if (!path.isExpressionStatement()) {
+ return false;
+ }
+
+ if (
+ !(path as NodePath<ExpressionStatement>)
+ .get('expression')
+ .isAssignmentExpression()
+ ) {
+ return false;
+ }
+
+ const left = path.get('expression.left');
+
+ if (!isMemberExpression(left)) {
+ return false;
+ }
+
+ const object = left.get('object');
+ const property = left.get('property');
+ if (
+ Array.isArray(property) ||
+ !property.isIdentifier() ||
+ property.node.name !== '__linariaPreval'
+ ) {
+ return false;
+ }
+
+ return object.isIdentifier() && object.node.name === 'exports';
+}
+
+const extractor: Evaluator = (filename, options, text, only = null) => {
+ const transformOptions = buildOptions(filename, options);
+ transformOptions.presets!.unshift([
+ require.resolve('@linaria/preeval'),
+ options,
+ ]);
+ transformOptions.plugins!.unshift([
+ require.resolve('@babel/plugin-transform-runtime'),
+ { useESModules: false },
+ ]);
+
+ // We made a mistake somewhen, and linaria preval was dependent on `plugin-transform-template-literals`
+ // Usually it was loaded into preval, because user was using `@babel/preset-env` preset which included that plugin. Internally we used this preset for tests (and previously for everything) - thats why we implemented behavior based on existing of that plugin
+ // The ordering is very important here, that's why it is added as a preset, not just as a plugin. It makes this plugin run *AFTER* linaria preset, which is required to make have the current behavior.
+ // In preval we have 2 visitors, one for Call Expressions and second for TaggedTemplateLiterals. Babel process TaggedTemplates first for some reason, and we grab only the css`` statements, we skip styled statements at this stage.
+ // Then it process TaggedTemplateLiterals with mentioned plugin, which transforms them to CallExpressions (babel seems to apply thw whole set of plugins for particular visitor, then for the next visitor and so on).
+ // Then Linaria can identify all `styled` as call expressions, including `styled.h1`, `styled.p` and others.
+
+ // Presets ordering is from last to first, so we add the plugin at the beginning of the list, which persist the order that was established with formerly used `@babel/preset-env`.
+
+ transformOptions.presets!.unshift({
+ plugins: [require.resolve('@babel/plugin-transform-template-literals')],
+ });
+ // Expressions will be extracted only for __linariaPreval.
+ // In all other cases a code will be returned as is.
+ let { code } = transformSync(text, transformOptions)!;
+ if (!only || only.length !== 1 || only[0] !== '__linariaPreval') {
+ return [code!, null];
+ }
+ // We cannot just use `ast` that was returned by `transformSync`,
+ // because there is some kind of cache inside `traverse` which
+ // reuses `NodePath` with a wrong scope.
+ // There is probably a better solution, but I haven't found it yet.
+ const ast = parseSync(code!, { filename: filename + '.preval' });
+ // First of all, let's find a __linariaPreval export
+ traverse(ast!, {
+ // We know that export has been added to the program body,
+ // so we don't need to traverse through the whole tree
+ Program(path: NodePath<Program>) {
+ const body = path.get('body');
+ // Highly likely it has been added in the end
+ for (let idx = body.length - 1; idx >= 0; idx--) {
+ if (isLinariaPrevalExport(body[idx])) {
+ // Here we are!
+ const statements = RequirementsResolver.resolve(
+ body[idx].get('expression.right')
+ );
+
+ // We only need to evaluate the last item in a sequence expression, e.g. (a, b, c)
+ body[idx].traverse({
+ SequenceExpression(sequence: NodePath<SequenceExpression>) {
+ sequence.replaceWith(
+ sequence.get('expressions')[
+ sequence.node.expressions.length - 1
+ ]
+ );
+ },
+ });
+
+ // We'll wrap each code in a block to avoid collisions in variable names
+ const wrapped = statements.reduce(
+ (acc, curr) => t.blockStatement([curr, acc]),
+ t.blockStatement([body[idx].node])
+ );
+
+ // Generate a new code with extracted statements
+ code = [
+ // Use String.raw to preserve escapes such as '\n' in the code
+ String.raw`${generator(wrapped).code}`,
+ ].join('\n');
+ break;
+ }
+ }
+
+ path.stop();
+ },
+ });
+
+ return [code!, null];
+};
+
+export default extractor;
diff --git a/@linaria/packages/extractor/tsconfig.json b/@linaria/packages/extractor/tsconfig.json
new file mode 100644
index 0000000..1b2c87e
--- /dev/null
+++ b/@linaria/packages/extractor/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": { "paths": {}, "rootDir": "src/" },
+ "references": [{ "path": "../babel" }, { "path": "../preeval" }]
+}