summaryrefslogtreecommitdiff
path: root/@linaria/packages/babel
diff options
context:
space:
mode:
Diffstat (limited to '@linaria/packages/babel')
-rw-r--r--@linaria/packages/babel/CHANGELOG.md55
-rw-r--r--@linaria/packages/babel/README.md35
-rw-r--r--@linaria/packages/babel/__fixtures__/complex-component.js21
-rw-r--r--@linaria/packages/babel/__fixtures__/components-library.js16
-rw-r--r--@linaria/packages/babel/__fixtures__/enums.ts3
-rw-r--r--@linaria/packages/babel/__fixtures__/escape-character.js7
-rw-r--r--@linaria/packages/babel/__fixtures__/sample-asset.png0
-rw-r--r--@linaria/packages/babel/__fixtures__/sample-data.json10
-rw-r--r--@linaria/packages/babel/__fixtures__/sample-script.js1
-rw-r--r--@linaria/packages/babel/__fixtures__/sample-typescript.tsx1
-rw-r--r--@linaria/packages/babel/__fixtures__/slugify.ts3
-rw-r--r--@linaria/packages/babel/__fixtures__/ts-data.ts4
-rw-r--r--@linaria/packages/babel/__tests__/__snapshots__/babel.test.ts.snap617
-rw-r--r--@linaria/packages/babel/__tests__/__snapshots__/dynamic-import-noop.test.js.snap8
-rw-r--r--@linaria/packages/babel/__tests__/__snapshots__/transform.test.ts.snap16
-rw-r--r--@linaria/packages/babel/__tests__/babel.test.ts524
-rw-r--r--@linaria/packages/babel/__tests__/depsGraph.test.ts356
-rw-r--r--@linaria/packages/babel/__tests__/dynamic-import-noop.test.js15
-rw-r--r--@linaria/packages/babel/__tests__/evaluators/__snapshots__/extractor.test.ts.snap237
-rw-r--r--@linaria/packages/babel/__tests__/evaluators/__snapshots__/preeval.test.ts.snap60
-rw-r--r--@linaria/packages/babel/__tests__/evaluators/__snapshots__/shaker.test.ts.snap213
-rw-r--r--@linaria/packages/babel/__tests__/evaluators/extractor.test.ts214
-rw-r--r--@linaria/packages/babel/__tests__/evaluators/preeval.test.ts121
-rw-r--r--@linaria/packages/babel/__tests__/evaluators/shaker.test.ts259
-rw-r--r--@linaria/packages/babel/__tests__/module.test.ts320
-rw-r--r--@linaria/packages/babel/__tests__/transform.test.ts237
-rw-r--r--@linaria/packages/babel/__utils__/linaria-snapshot-serializer.ts26
-rw-r--r--@linaria/packages/babel/__utils__/strategy-tester.ts976
-rw-r--r--@linaria/packages/babel/babel.config.js3
-rw-r--r--@linaria/packages/babel/package.json60
-rw-r--r--@linaria/packages/babel/src/babel.ts3
-rw-r--r--@linaria/packages/babel/src/dynamic-import-noop.ts26
-rw-r--r--@linaria/packages/babel/src/eval-cache.ts87
-rw-r--r--@linaria/packages/babel/src/evaluators/buildOptions.ts104
-rw-r--r--@linaria/packages/babel/src/evaluators/index.ts22
-rw-r--r--@linaria/packages/babel/src/evaluators/templateProcessor.ts313
-rw-r--r--@linaria/packages/babel/src/evaluators/visitors/JSXElement.ts56
-rw-r--r--@linaria/packages/babel/src/evaluators/visitors/ProcessCSS.ts20
-rw-r--r--@linaria/packages/babel/src/evaluators/visitors/ProcessStyled.ts46
-rw-r--r--@linaria/packages/babel/src/extract.ts222
-rw-r--r--@linaria/packages/babel/src/index.ts45
-rw-r--r--@linaria/packages/babel/src/module.ts373
-rw-r--r--@linaria/packages/babel/src/plugin-syntax-dynamic-import.d.ts4
-rw-r--r--@linaria/packages/babel/src/process.ts31
-rw-r--r--@linaria/packages/babel/src/transform.ts187
-rw-r--r--@linaria/packages/babel/src/types.ts180
-rw-r--r--@linaria/packages/babel/src/units.ts101
-rw-r--r--@linaria/packages/babel/src/utils/getLinariaComment.ts31
-rw-r--r--@linaria/packages/babel/src/utils/getVisitorKeys.ts10
-rw-r--r--@linaria/packages/babel/src/utils/hasImport.ts83
-rw-r--r--@linaria/packages/babel/src/utils/isBoxedPrimitive.ts10
-rw-r--r--@linaria/packages/babel/src/utils/isNode.ts5
-rw-r--r--@linaria/packages/babel/src/utils/isSerializable.ts11
-rw-r--r--@linaria/packages/babel/src/utils/isStyledOrCss.ts67
-rw-r--r--@linaria/packages/babel/src/utils/loadOptions.ts38
-rw-r--r--@linaria/packages/babel/src/utils/peek.ts3
-rw-r--r--@linaria/packages/babel/src/utils/slugify.ts83
-rw-r--r--@linaria/packages/babel/src/utils/stripLines.ts23
-rw-r--r--@linaria/packages/babel/src/utils/throwIfInvalid.ts49
-rw-r--r--@linaria/packages/babel/src/utils/toCSS.ts57
-rw-r--r--@linaria/packages/babel/src/utils/toValidCSSIdentifier.ts3
-rw-r--r--@linaria/packages/babel/src/visitors/CollectDependencies.ts132
-rw-r--r--@linaria/packages/babel/src/visitors/DetectStyledImportName.ts33
-rw-r--r--@linaria/packages/babel/src/visitors/GenerateClassNames.ts161
-rw-r--r--@linaria/packages/babel/tsconfig.json8
65 files changed, 7045 insertions, 0 deletions
diff --git a/@linaria/packages/babel/CHANGELOG.md b/@linaria/packages/babel/CHANGELOG.md
new file mode 100644
index 0000000..fc18483
--- /dev/null
+++ b/@linaria/packages/babel/CHANGELOG.md
@@ -0,0 +1,55 @@
+# 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.7](https://github.com/callstack/linaria/compare/v3.0.0-beta.6...v3.0.0-beta.7) (2021-06-24)
+
+
+### Bug Fixes
+
+* **shaker:** fix undefined imports in some cases ([#333](https://github.com/callstack/linaria/issues/333), [#761](https://github.com/callstack/linaria/issues/761)) ([#787](https://github.com/callstack/linaria/issues/787)) ([e374072](https://github.com/callstack/linaria/commit/e3740727447b2867a2cfe40f763bc88e72eb2503))
+
+
+
+
+
+# [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/babel-preset
+
+
+
+
+
+# [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/babel-preset
+
+
+
+
+
+# [3.0.0-beta.2](https://github.com/callstack/linaria/compare/v3.0.0-beta.1...v3.0.0-beta.2) (2021-04-11)
+
+
+### Bug Fixes
+
+* Import custom identifier called css with error "Cannot find module 'linaria'" [#739](https://github.com/callstack/linaria/issues/739) ([#740](https://github.com/callstack/linaria/issues/740)) ([07fb381](https://github.com/callstack/linaria/commit/07fb38131c9dec406dcca72f45638561c815e824))
+* loadOptions text regex ([#728](https://github.com/callstack/linaria/issues/728)) ([34ca3e5](https://github.com/callstack/linaria/commit/34ca3e5f211b65c14c2bf4efabb7065f7109da23))
+
+
+### Features
+
+* **babel:** expose CSS extraction from AST logic ([#737](https://github.com/callstack/linaria/issues/737)) ([f049a11](https://github.com/callstack/linaria/commit/f049a119ef70346340676ab6a397ad6358e5f39b))
diff --git a/@linaria/packages/babel/README.md b/@linaria/packages/babel/README.md
new file mode 100644
index 0000000..0d75b37
--- /dev/null
+++ b/@linaria/packages/babel/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/babel/__fixtures__/complex-component.js b/@linaria/packages/babel/__fixtures__/complex-component.js
new file mode 100644
index 0000000..0c9ae15
--- /dev/null
+++ b/@linaria/packages/babel/__fixtures__/complex-component.js
@@ -0,0 +1,21 @@
+// 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 = styled.h1`
+ ${foo('font')}
+ ${foo('box')}
+`;
diff --git a/@linaria/packages/babel/__fixtures__/components-library.js b/@linaria/packages/babel/__fixtures__/components-library.js
new file mode 100644
index 0000000..487648e
--- /dev/null
+++ b/@linaria/packages/babel/__fixtures__/components-library.js
@@ -0,0 +1,16 @@
+import { styled } from '@linaria/react';
+
+export const T1 = styled.h1`
+ background: #111;
+`;
+export const T2 = styled.h2`
+ background: #222;
+`;
+export const T3 = styled.h3`
+ ${T2} {
+ background: #333;
+ }
+`;
+export default styled.p`
+ background: #333;
+`;
diff --git a/@linaria/packages/babel/__fixtures__/enums.ts b/@linaria/packages/babel/__fixtures__/enums.ts
new file mode 100644
index 0000000..aa87847
--- /dev/null
+++ b/@linaria/packages/babel/__fixtures__/enums.ts
@@ -0,0 +1,3 @@
+export enum Colors {
+ BLUE = '#27509A',
+}
diff --git a/@linaria/packages/babel/__fixtures__/escape-character.js b/@linaria/packages/babel/__fixtures__/escape-character.js
new file mode 100644
index 0000000..c7f7deb
--- /dev/null
+++ b/@linaria/packages/babel/__fixtures__/escape-character.js
@@ -0,0 +1,7 @@
+import { styled } from '@linaria/react';
+
+const selectors = ['a', 'b'];
+
+export const Block = styled.div`
+ ${selectors.map((c) => String.raw`${c} { content: "\u000A"; }`).join('\n')};
+`;
diff --git a/@linaria/packages/babel/__fixtures__/sample-asset.png b/@linaria/packages/babel/__fixtures__/sample-asset.png
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/@linaria/packages/babel/__fixtures__/sample-asset.png
diff --git a/@linaria/packages/babel/__fixtures__/sample-data.json b/@linaria/packages/babel/__fixtures__/sample-data.json
new file mode 100644
index 0000000..c623402
--- /dev/null
+++ b/@linaria/packages/babel/__fixtures__/sample-data.json
@@ -0,0 +1,10 @@
+{
+ "name": "Luke Skywalker",
+ "height": "172",
+ "mass": "77",
+ "hair_color": "blond",
+ "skin_color": "fair",
+ "eye_color": "blue",
+ "birth_year": "19BBY",
+ "gender": "male"
+}
diff --git a/@linaria/packages/babel/__fixtures__/sample-script.js b/@linaria/packages/babel/__fixtures__/sample-script.js
new file mode 100644
index 0000000..888cae3
--- /dev/null
+++ b/@linaria/packages/babel/__fixtures__/sample-script.js
@@ -0,0 +1 @@
+module.exports = 42;
diff --git a/@linaria/packages/babel/__fixtures__/sample-typescript.tsx b/@linaria/packages/babel/__fixtures__/sample-typescript.tsx
new file mode 100644
index 0000000..d33d509
--- /dev/null
+++ b/@linaria/packages/babel/__fixtures__/sample-typescript.tsx
@@ -0,0 +1 @@
+export default 27;
diff --git a/@linaria/packages/babel/__fixtures__/slugify.ts b/@linaria/packages/babel/__fixtures__/slugify.ts
new file mode 100644
index 0000000..bc69242
--- /dev/null
+++ b/@linaria/packages/babel/__fixtures__/slugify.ts
@@ -0,0 +1,3 @@
+import slugify from '../src/utils/slugify';
+
+export default slugify;
diff --git a/@linaria/packages/babel/__fixtures__/ts-data.ts b/@linaria/packages/babel/__fixtures__/ts-data.ts
new file mode 100644
index 0000000..222fe53
--- /dev/null
+++ b/@linaria/packages/babel/__fixtures__/ts-data.ts
@@ -0,0 +1,4 @@
+export enum TestEnum {
+ FirstValue,
+ SecondValue,
+}
diff --git a/@linaria/packages/babel/__tests__/__snapshots__/babel.test.ts.snap b/@linaria/packages/babel/__tests__/__snapshots__/babel.test.ts.snap
new file mode 100644
index 0000000..84be71a
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/__snapshots__/babel.test.ts.snap
@@ -0,0 +1,617 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`does not include styles if not referenced anywhere 1`] = `
+"import { css } from '@linaria/core';
+import { styled } from '@linaria/react';
+const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\"
+});
+const title = \\"tccybe9\\";"
+`;
+
+exports[`does not include styles if not referenced anywhere 2`] = `Object {}`;
+
+exports[`does not output CSS if none present 1`] = `
+"const number = 42;
+const title = String.raw\`This is something\`;"
+`;
+
+exports[`does not output CSS if none present 2`] = `Object {}`;
+
+exports[`does not output CSS property when value is a blank string 1`] = `
+"import { css } from '@linaria/core';
+export const title = \\"t17gu1mi\\";"
+`;
+
+exports[`does not output CSS property when value is a blank string 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ font-size: ;
+ margin: 6px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`evaluates and inlines expressions in scope 1`] = `
+"import { styled } from '@linaria/react';
+const color = 'blue';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\"
+});"
+`;
+
+exports[`evaluates and inlines expressions in scope 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ color: blue;
+ width: 33.333333333333336%;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`handles css template literal in JSX element 1`] = `
+"import { css } from '@linaria/core';
+<Title class={\\"t17gu1mi\\"} />;"
+`;
+
+exports[`handles css template literal in JSX element 2`] = `
+
+CSS:
+
+.t17gu1mi { font-size: 14px; }
+
+Dependencies: NA
+
+`;
+
+exports[`handles css template literal in object property 1`] = `
+"import { css } from '@linaria/core';
+const components = {
+ title: \\"t17gu1mi\\"
+};"
+`;
+
+exports[`handles css template literal in object property 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ font-size: 14px;
+ }
+
+Dependencies: NA
+
+`;
+
+exports[`handles fn passed in as classNameSlug 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled('h1')({
+ name: \\"Title\\",
+ class: \\"t17gu1mi_42_Title\\"
+});"
+`;
+
+exports[`handles fn passed in as classNameSlug 2`] = `
+
+CSS:
+
+.t17gu1mi_42_Title {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`handles interpolation followed by unit 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\",
+ vars: {
+ \\"t17gu1mi-0\\": [size, \\"em\\"],
+ \\"t17gu1mi-1\\": [shadow, \\"px\\"],
+ \\"t17gu1mi-2\\": [size, \\"px\\"],
+ \\"t17gu1mi-3\\": [props => props.width, \\"vw\\"],
+ \\"t17gu1mi-4\\": [props => {
+ if (true) {
+ return props.height;
+ } else {
+ return 200;
+ }
+ }, \\"px\\"],
+ \\"t17gu1mi-5\\": [unit, \\"fr\\"],
+ \\"t17gu1mi-7\\": [function (props) {
+ return 200;
+ }, \\"px\\"]
+ }
+});"
+`;
+
+exports[`handles interpolation followed by unit 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ font-size: var(--t17gu1mi-0);
+ text-shadow: black 1px var(--t17gu1mi-1), white -2px -2px;
+ margin: var(--t17gu1mi-2);
+ width: calc(2 * var(--t17gu1mi-3));
+ height: var(--t17gu1mi-4);
+ grid-template-columns: var(--t17gu1mi-5) 1fr 1fr var(--t17gu1mi-5);
+ border-radius: var(--t17gu1mi-7)
+}
+
+Dependencies: NA
+
+`;
+
+exports[`handles nested blocks 1`] = `
+"import { styled } from '@linaria/react';
+export const Button = /*#__PURE__*/styled(\\"button\\")({
+ name: \\"Button\\",
+ class: \\"b17gu1mi\\",
+ vars: {
+ \\"b17gu1mi-0\\": [regular]
+ }
+});"
+`;
+
+exports[`handles nested blocks 2`] = `
+
+CSS:
+
+.b17gu1mi {
+ font-family: var(--b17gu1mi-0);
+
+ &:hover {
+ border-color: blue;
+ }
+
+ @media (max-width: 200px) {
+ width: 100%;
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`handles objects with enums as keys 1`] = `
+"import { css } from '@linaria/core';
+import { TestEnum } from './ts-data.ts';
+export const object = {
+ [TestEnum.FirstValue]: \\"t17gu1mi\\",
+ [TestEnum.SecondValue]: \\"tccybe9\\"
+};"
+`;
+
+exports[`handles objects with enums as keys 2`] = `
+
+CSS:
+
+.t17gu1mi {}
+.tccybe9 {}
+
+Dependencies: NA
+
+`;
+
+exports[`handles objects with numeric keys 1`] = `
+"import { css } from '@linaria/core';
+export const object = {
+ stringKey: \\"s17gu1mi\\",
+ 42: \\"_ccybe9\\"
+};"
+`;
+
+exports[`handles objects with numeric keys 2`] = `
+
+CSS:
+
+.s17gu1mi {}
+._ccybe9 {}
+
+Dependencies: NA
+
+`;
+
+exports[`includes unreferenced styles for :global 1`] = `
+"import { css } from '@linaria/core';
+import { styled } from '@linaria/react';
+const a = \\"a17gu1mi\\";
+const B = /*#__PURE__*/styled(\\"div\\")({
+ name: \\"B\\",
+ class: \\"bccybe9\\"
+});"
+`;
+
+exports[`includes unreferenced styles for :global 2`] = `
+
+CSS:
+
+.a17gu1mi {
+ :global() {
+ .title {
+ font-size: 14px;
+ }
+ }
+}
+.bccybe9 {
+ :global(.title) {
+ font-size: 14px;
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`inlines array styles as CSS string 1`] = `
+"import { styled } from '@linaria/react';
+const styles = [{
+ flex: 1
+}, {
+ display: 'block',
+ height: 24
+}];
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\"
+});"
+`;
+
+exports[`inlines array styles as CSS string 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ flex: 1; display: block; height: 24px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`inlines object styles as CSS string 1`] = `
+"import { styled } from '@linaria/react';
+const cover = {
+ '--color-primaryText': '#222',
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ opacity: 1,
+ minHeight: 420,
+ '&.shouldNotBeChanged': {
+ borderColor: '#fff'
+ },
+ '@media (min-width: 200px)': {
+ WebkitOpacity: .8,
+ MozOpacity: .8,
+ msOpacity: .8,
+ OOpacity: .8,
+ WebkitBorderRadius: 2,
+ MozBorderRadius: 2,
+ msBorderRadius: 2,
+ OBorderRadius: 2,
+ WebkitTransition: '400ms',
+ MozTransition: '400ms',
+ OTransition: '400ms',
+ msTransition: '400ms'
+ }
+};
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\"
+});"
+`;
+
+exports[`inlines object styles as CSS string 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ --color-primaryText: #222; position: absolute; top: 0; right: 0; bottom: 0; left: 0; opacity: 1; min-height: 420px; &.shouldNotBeChanged { border-color: #fff; } @media (min-width: 200px) { -webkit-opacity: 0.8; -moz-opacity: 0.8; -ms-opacity: 0.8; -o-opacity: 0.8; -webkit-border-radius: 2px; -moz-border-radius: 2px; -ms-border-radius: 2px; -o-border-radius: 2px; -webkit-transition: 400ms; -moz-transition: 400ms; -o-transition: 400ms; -ms-transition: 400ms; }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`outputs valid CSS classname 1`] = `
+"import { styled } from '@linaria/react';
+export const ΩPage$Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"\\\\u03A9Page$Title\\",
+ class: \\"\\\\u03C917gu1mi\\"
+});"
+`;
+
+exports[`outputs valid CSS classname 2`] = `
+
+CSS:
+
+.ω17gu1mi {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`prevents class name collision 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\",
+ vars: {
+ \\"t17gu1mi-0\\": [size, \\"px\\"],
+ \\"t17gu1mi-1\\": [props => props.color]
+ }
+});
+
+function Something() {
+ const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"tccybe9\\",
+ vars: {
+ \\"tccybe9-0\\": [regular]
+ }
+ });
+ return <Title />;
+}"
+`;
+
+exports[`prevents class name collision 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ font-size: var(--t17gu1mi-0);
+ color: var(--t17gu1mi-1)
+}
+.tccybe9 {
+ font-family: var(--tccybe9-0);
+ }
+
+Dependencies: NA
+
+`;
+
+exports[`replaces unknown expressions with CSS custom properties 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\",
+ vars: {
+ \\"t17gu1mi-0\\": [size, \\"px\\"],
+ \\"t17gu1mi-1\\": [props => props.color]
+ }
+});"
+`;
+
+exports[`replaces unknown expressions with CSS custom properties 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ font-size: var(--t17gu1mi-0);
+ color: var(--t17gu1mi-1);
+}
+
+Dependencies: NA
+
+`;
+
+exports[`supports both css and styled tags 1`] = `
+"import { css } from '@linaria/core';
+import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\"
+});
+export const title = \\"tccybe9\\";"
+`;
+
+exports[`supports both css and styled tags 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ font-size: 14px;
+}
+.tccybe9 {
+ color: blue;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`throws when contains dynamic expression without evaluate: true in css tag 1`] = `
+"<<DIRNAME>>/app/index.js: The CSS cannot contain JavaScript expressions when using the 'css' tag. To evaluate the expressions at build time, pass 'evaluate: true' to the babel plugin.
+ 2 |
+ 3 | const title = css\`
+> 4 | font-size: \${size}px;
+ | ^^^^
+ 5 | \`;"
+`;
+
+exports[`transpiles css template literal 1`] = `
+"import { css } from '@linaria/core';
+export const title = \\"t17gu1mi\\";"
+`;
+
+exports[`transpiles css template literal 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`transpiles renamed styled import 1`] = `
+"import { styled as custom } from '@linaria/react';
+export const Title = /*#__PURE__*/custom('h1')({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\"
+});"
+`;
+
+exports[`transpiles renamed styled import 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`transpiles styled template literal with function and component 1`] = `
+"import { styled } from '@linaria/react';
+
+const Heading = () => null;
+
+export const Title = /*#__PURE__*/styled(Heading)({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\"
+});"
+`;
+
+exports[`transpiles styled template literal with function and component 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`transpiles styled template literal with function and tag 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled('h1')({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\"
+});"
+`;
+
+exports[`transpiles styled template literal with function and tag 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`transpiles styled template literal with object 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"t17gu1mi\\"
+});"
+`;
+
+exports[`transpiles styled template literal with object 2`] = `
+
+CSS:
+
+.t17gu1mi {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`uses string passed in as classNameSlug 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled('h1')({
+ name: \\"Title\\",
+ class: \\"testSlug\\"
+});"
+`;
+
+exports[`uses string passed in as classNameSlug 2`] = `
+
+CSS:
+
+.testSlug {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`uses the same custom property for the same expression 1`] = `
+"import { styled } from '@linaria/react';
+export const Box = /*#__PURE__*/styled(\\"div\\")({
+ name: \\"Box\\",
+ class: \\"b17gu1mi\\",
+ vars: {
+ \\"b17gu1mi-0\\": [props => props.size, \\"px\\"]
+ }
+});"
+`;
+
+exports[`uses the same custom property for the same expression 2`] = `
+
+CSS:
+
+.b17gu1mi {
+ height: var(--b17gu1mi-0);
+ width: var(--b17gu1mi-0);
+}
+
+Dependencies: NA
+
+`;
+
+exports[`uses the same custom property for the same identifier 1`] = `
+"import { styled } from '@linaria/react';
+export const Box = /*#__PURE__*/styled(\\"div\\")({
+ name: \\"Box\\",
+ class: \\"b17gu1mi\\",
+ vars: {
+ \\"b17gu1mi-0\\": [size, \\"px\\"]
+ }
+});"
+`;
+
+exports[`uses the same custom property for the same identifier 2`] = `
+
+CSS:
+
+.b17gu1mi {
+ height: var(--b17gu1mi-0);
+ width: var(--b17gu1mi-0);
+}
+
+Dependencies: NA
+
+`;
diff --git a/@linaria/packages/babel/__tests__/__snapshots__/dynamic-import-noop.test.js.snap b/@linaria/packages/babel/__tests__/__snapshots__/dynamic-import-noop.test.js.snap
new file mode 100644
index 0000000..02d59d1
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/__snapshots__/dynamic-import-noop.test.js.snap
@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`replaces dynamic imports with a noop 1`] = `
+"({
+ then: () => undefined,
+ catch: () => undefined
+}).then(foo => foo.init());"
+`;
diff --git a/@linaria/packages/babel/__tests__/__snapshots__/transform.test.ts.snap b/@linaria/packages/babel/__tests__/__snapshots__/transform.test.ts.snap
new file mode 100644
index 0000000..77636ca
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/__snapshots__/transform.test.ts.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`doesn't rewrite an absolute path in url() declarations 1`] = `
+".tpyglzj{background-image:url(/assets/test.jpg);}
+"
+`;
+
+exports[`rewrites a relative path in url() declarations 1`] = `
+".tpyglzj{background-image:url(../assets/test.jpg);background-image:url(\\"../assets/test.jpg\\");background-image:url('../assets/test.jpg');}
+"
+`;
+
+exports[`rewrites multiple relative paths in url() declarations 1`] = `
+"@font-face{font-family:Test;src:url(../assets/font.woff2) format(\\"woff2\\"),url(../assets/font.woff) format(\\"woff\\");}
+"
+`;
diff --git a/@linaria/packages/babel/__tests__/babel.test.ts b/@linaria/packages/babel/__tests__/babel.test.ts
new file mode 100644
index 0000000..99cc816
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/babel.test.ts
@@ -0,0 +1,524 @@
+import { join } from 'path';
+import { transformAsync } from '@babel/core';
+import dedent from 'dedent';
+import stripAnsi from 'strip-ansi';
+import type { StrictOptions } from '../src';
+import serializer from '../__utils__/linaria-snapshot-serializer';
+
+expect.addSnapshotSerializer(serializer);
+
+const transpile = async (
+ input: string,
+ opts: Partial<StrictOptions> = { evaluate: false }
+) =>
+ (await transformAsync(input, {
+ babelrc: false,
+ presets: [[require.resolve('../src'), opts]],
+ plugins: ['@babel/plugin-syntax-jsx'],
+ filename: join(__dirname, 'app/index.js'),
+ configFile: false,
+ }))!;
+
+it('transpiles styled template literal with object', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Title = styled.h1\`
+ font-size: 14px;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('uses string passed in as classNameSlug', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Title = styled('h1')\`
+ font-size: 14px;
+ \`;
+`,
+ { classNameSlug: 'testSlug' }
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('handles fn passed in as classNameSlug', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Title = styled('h1')\`
+ font-size: 14px;
+ \`;
+`,
+ {
+ classNameSlug: (hash, title) => {
+ return `${hash}_${7 * 6}_${title}`;
+ },
+ }
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('transpiles styled template literal with function and tag', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Title = styled('h1')\`
+ font-size: 14px;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('transpiles renamed styled import', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled as custom } from '@linaria/react';
+
+ export const Title = custom('h1')\`
+ font-size: 14px;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('transpiles styled template literal with function and component', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+ const Heading = () => null;
+
+ export const Title = styled(Heading)\`
+ font-size: 14px;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('outputs valid CSS classname', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const ΩPage$Title = styled.h1\`
+ font-size: 14px;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('evaluates and inlines expressions in scope', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const color = 'blue';
+
+ export const Title = styled.h1\`
+ color: ${'${color}'};
+ width: ${'${100 / 3}'}%;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('inlines object styles as CSS string', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const cover = {
+ '--color-primaryText': '#222',
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ opacity: 1,
+ minHeight: 420,
+
+ '&.shouldNotBeChanged': {
+ borderColor: '#fff',
+ },
+
+ '@media (min-width: 200px)': {
+ WebkitOpacity: .8,
+ MozOpacity: .8,
+ msOpacity: .8,
+ OOpacity: .8,
+ WebkitBorderRadius: 2,
+ MozBorderRadius: 2,
+ msBorderRadius: 2,
+ OBorderRadius: 2,
+ WebkitTransition: '400ms',
+ MozTransition: '400ms',
+ OTransition: '400ms',
+ msTransition: '400ms',
+ }
+ };
+
+ export const Title = styled.h1\`
+ ${'${cover}'}
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('inlines array styles as CSS string', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const styles = [
+ { flex: 1 },
+ { display: 'block', height: 24 },
+ ];
+
+ export const Title = styled.h1\`
+ ${'${styles}'}
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('replaces unknown expressions with CSS custom properties', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Title = styled.h1\`
+ font-size: ${'${size}'}px;
+ color: ${'${props => props.color}'};
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('handles interpolation followed by unit', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Title = styled.h1\`
+ font-size: ${'${size}'}em;
+ text-shadow: black 1px ${'${shadow}'}px, white -2px -2px;
+ margin: ${'${size}'}px;
+ width: calc(2 * ${'${props => props.width}'}vw);
+ height: ${'${props => { if (true) { return props.height } else { return 200 } }}'}px;
+ grid-template-columns: ${'${unit}'}fr 1fr 1fr ${'${unit}'}fr;
+ border-radius: ${'${function(props) { return 200 }}'}px
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('uses the same custom property for the same identifier', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Box = styled.div\`
+ height: ${'${size}'}px;
+ width: ${'${size}'}px;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('uses the same custom property for the same expression', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Box = styled.div\`
+ height: ${'${props => props.size}'}px;
+ width: ${'${props => props.size}'}px;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('handles nested blocks', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Button = styled.button\`
+ font-family: ${'${regular}'};
+
+ &:hover {
+ border-color: blue;
+ }
+
+ @media (max-width: 200px) {
+ width: 100%;
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('prevents class name collision', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Title = styled.h1\`
+ font-size: ${'${size}'}px;
+ color: ${'${props => props.color}'}
+ \`;
+
+ function Something() {
+ const Title = styled.h1\`
+ font-family: ${'${regular}'};
+ \`;
+
+ return <Title />;
+ }
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('does not output CSS if none present', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ const number = 42;
+
+ const title = String.raw\`This is something\`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('does not output CSS property when value is a blank string', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+
+ export const title = css\`
+ font-size: ${''};
+ margin: 6px;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('transpiles css template literal', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+
+ export const title = css\`
+ font-size: 14px;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('handles css template literal in object property', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+
+ const components = {
+ title: css\`
+ font-size: 14px;
+ \`
+ };
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('handles css template literal in JSX element', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+
+ <Title class={css\` font-size: 14px; \`} />
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('throws when contains dynamic expression without evaluate: true in css tag', async () => {
+ expect.assertions(1);
+
+ try {
+ await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+
+ const title = css\`
+ font-size: ${'${size}'}px;
+ \`;
+ `
+ );
+ } catch (e) {
+ expect(
+ stripAnsi(e.message.replace(__dirname, '<<DIRNAME>>'))
+ ).toMatchSnapshot();
+ }
+});
+
+it('supports both css and styled tags', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+ import { styled } from '@linaria/react';
+
+ export const Title = styled.h1\`
+ font-size: 14px;
+ \`;
+
+ export const title = css\`
+ color: blue;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('does not include styles if not referenced anywhere', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+ import { styled } from '@linaria/react';
+
+ const Title = styled.h1\`
+ font-size: 14px;
+ \`;
+
+ const title = css\`
+ color: blue;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('includes unreferenced styles for :global', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+ import { styled } from '@linaria/react';
+
+ const a = css\`
+ :global() {
+ .title {
+ font-size: 14px;
+ }
+ }
+ \`;
+
+ const B = styled.div\`
+ :global(.title) {
+ font-size: 14px;
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('handles objects with numeric keys', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+
+ export const object = {
+ stringKey: css\`\`,
+ 42: css\`\`,
+ }
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
+
+it('handles objects with enums as keys', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+ import { TestEnum } from './ts-data.ts';
+
+ export const object = {
+ [TestEnum.FirstValue]: css\`\`,
+ [TestEnum.SecondValue]: css\`\`,
+ }
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+});
diff --git a/@linaria/packages/babel/__tests__/depsGraph.test.ts b/@linaria/packages/babel/__tests__/depsGraph.test.ts
new file mode 100644
index 0000000..000bd30
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/depsGraph.test.ts
@@ -0,0 +1,356 @@
+/* eslint-disable no-template-curly-in-string */
+
+import dedent from 'dedent';
+import * as babel from '@babel/core';
+import buildDepsGraph from '../../shaker/src/graphBuilder';
+
+function _build(literal: TemplateStringsArray, ...placeholders: string[]) {
+ const code = dedent(literal, ...placeholders);
+ return {
+ ast: babel.parseSync(code, { filename: 'source.js' })!,
+ code,
+ };
+}
+
+function _buildGraph(literal: TemplateStringsArray, ...placeholders: string[]) {
+ const { ast } = _build(literal, ...placeholders);
+ return buildDepsGraph(ast);
+}
+
+describe('VariableDeclaration', () => {
+ it('Identifier', () => {
+ const graph = _buildGraph`
+ const a = 42;
+ `;
+
+ const deps = graph.getDependenciesByBinding('0:a');
+ expect(deps).toMatchObject([
+ {
+ type: 'VariableDeclarator',
+ },
+ {
+ type: 'NumericLiteral',
+ value: 42,
+ },
+ ]);
+
+ expect(graph.findDependencies({ name: 'a' })).toContainEqual(deps[0]);
+ });
+});
+
+describe('scopes', () => {
+ it('BlockStatement', () => {
+ const graph = _buildGraph`
+ let a = 42;
+ {
+ const a = "21";
+ }
+
+ a = 21;
+ `;
+
+ const deps0 = graph.getDependenciesByBinding('0:a');
+ expect(deps0).toMatchObject([
+ {
+ type: 'VariableDeclarator',
+ },
+ {
+ type: 'NumericLiteral',
+ value: 42,
+ },
+ {
+ type: 'Identifier',
+ start: 4,
+ },
+ {
+ type: 'Identifier',
+ start: 35,
+ },
+ {
+ type: 'AssignmentExpression',
+ },
+ {
+ type: 'NumericLiteral',
+ value: 21,
+ },
+ ]);
+
+ const deps1 = graph.getDependenciesByBinding('1:a');
+ expect(deps1).toMatchObject([
+ {
+ type: 'VariableDeclarator',
+ },
+ {
+ type: 'StringLiteral',
+ value: '21',
+ },
+ ]);
+
+ expect(graph.findDependencies({ name: 'a' })).toHaveLength(8);
+ });
+
+ it('Function', () => {
+ const graph = _buildGraph`
+ const a = (arg1, arg2, arg3) => arg1 + arg2 + arg3;
+ `;
+
+ const aDeps = graph.getDependenciesByBinding('0:a');
+ expect(aDeps).toMatchObject([
+ {
+ type: 'VariableDeclarator',
+ },
+ {
+ type: 'ArrowFunctionExpression',
+ },
+ ]);
+
+ expect(graph.getDependenciesByBinding('1:arg1')).toHaveLength(3);
+ expect(graph.getDependentsByBinding('1:arg1')).toMatchObject([
+ {
+ // arg1 in the binary expression
+ type: 'Identifier',
+ name: 'arg1',
+ start: 32,
+ },
+ {
+ // arg1 + arg2
+ type: 'BinaryExpression',
+ right: {
+ name: 'arg2',
+ },
+ },
+ {
+ // (arg1 + arg2) + arg3, because of it is the whole function body
+ type: 'BinaryExpression',
+ right: {
+ name: 'arg3',
+ },
+ },
+ ]);
+
+ expect(graph.getDependenciesByBinding('1:arg2')).toMatchObject([
+ {
+ type: 'ArrowFunctionExpression',
+ },
+ {
+ type: 'Identifier',
+ name: 'arg2',
+ start: 17,
+ },
+ {
+ type: 'BinaryExpression',
+ start: 32,
+ },
+ ]);
+
+ expect(graph.getDependenciesByBinding('1:arg3')).toMatchObject([
+ {
+ type: 'ArrowFunctionExpression',
+ },
+ {
+ type: 'Identifier',
+ name: 'arg3',
+ start: 23,
+ },
+ {
+ type: 'BinaryExpression',
+ start: 32,
+ },
+ ]);
+ });
+});
+
+describe('AssignmentExpression', () => {
+ it('Identifier', () => {
+ const graph = _buildGraph`
+ let a = 42;
+ a = 24;
+ `;
+
+ const deps = graph.getDependenciesByBinding('0:a');
+ expect(deps).toMatchObject([
+ {
+ type: 'VariableDeclarator',
+ },
+ {
+ type: 'NumericLiteral',
+ value: 42,
+ },
+ {
+ type: 'Identifier',
+ name: 'a',
+ start: 4,
+ },
+ {
+ type: 'Identifier',
+ name: 'a',
+ start: 12,
+ },
+ {
+ type: 'AssignmentExpression',
+ },
+ {
+ type: 'NumericLiteral',
+ value: 24,
+ },
+ ]);
+
+ expect(graph.findDependents({ value: 42 })).toHaveLength(1);
+ expect(graph.findDependents({ value: 24 })).toHaveLength(1);
+ });
+
+ it('MemberExpression', () => {
+ const graph = _buildGraph`
+ const a = {};
+ a.foo.bar = 42;
+ `;
+
+ expect(graph.getDependenciesByBinding('0:a')).toMatchObject([
+ {
+ type: 'VariableDeclarator',
+ },
+ {
+ type: 'ObjectExpression',
+ properties: [],
+ },
+ {
+ type: 'Identifier',
+ name: 'a',
+ start: 6,
+ },
+ {
+ type: 'Identifier',
+ name: 'a',
+ start: 14,
+ },
+ {
+ type: 'MemberExpression',
+ property: {
+ name: 'foo',
+ },
+ },
+ ]);
+
+ expect(graph.findDependents({ value: 42 })).toMatchObject([
+ {
+ type: 'MemberExpression',
+ property: {
+ name: 'bar',
+ },
+ },
+ ]);
+ });
+});
+
+it('SequenceExpression', () => {
+ const graph = _buildGraph`
+ const color1 = 'blue';
+ let local = '';
+ const color2 = (true, local = color1, () => local);
+ `;
+
+ const seqDeps = graph.findDependencies({
+ type: 'SequenceExpression',
+ });
+ expect(seqDeps).toMatchObject([
+ {
+ type: 'ArrowFunctionExpression',
+ },
+ {
+ id: {
+ name: 'color2',
+ },
+ type: 'VariableDeclarator',
+ },
+ ]);
+
+ const fnDeps = graph.findDependencies({
+ type: 'ArrowFunctionExpression',
+ });
+ expect(fnDeps).toMatchObject([
+ {
+ body: {
+ name: 'local',
+ type: 'Identifier',
+ },
+
+ type: 'ArrowFunctionExpression',
+ },
+ {
+ name: 'local',
+ type: 'Identifier',
+ },
+ {
+ type: 'SequenceExpression',
+ },
+ ]);
+
+ const localDeps = graph.getDependenciesByBinding('0:local');
+ expect(localDeps).toMatchObject([
+ {
+ type: 'VariableDeclarator',
+ },
+ {
+ type: 'StringLiteral',
+ value: '',
+ },
+ {
+ type: 'Identifier',
+ name: 'local',
+ start: 27,
+ },
+ {
+ type: 'Identifier',
+ name: 'local',
+ start: 61,
+ },
+ {
+ type: 'AssignmentExpression',
+ },
+ {
+ type: 'Identifier',
+ name: 'color1',
+ },
+ {
+ type: 'Identifier',
+ name: 'local',
+ start: 27,
+ },
+ {
+ type: 'ArrowFunctionExpression',
+ },
+ ]);
+
+ const bool = { type: 'BooleanLiteral' };
+ expect(graph.findDependents(bool)).toHaveLength(0);
+ expect(graph.findDependencies(bool)).toHaveLength(1);
+});
+
+it('MemberExpression', () => {
+ const graph = _buildGraph`
+ const key = 'blue';
+ const obj = { blue: '#00F' };
+ const blue = obj[key];
+ `;
+
+ const memberExprDeps = graph.findDependencies({
+ type: 'MemberExpression',
+ });
+
+ expect(memberExprDeps).toMatchObject([
+ {
+ type: 'Identifier',
+ name: 'obj',
+ },
+ {
+ type: 'Identifier',
+ name: 'key',
+ },
+ {
+ type: 'VariableDeclarator',
+ id: {
+ name: 'blue',
+ },
+ },
+ ]);
+});
diff --git a/@linaria/packages/babel/__tests__/dynamic-import-noop.test.js b/@linaria/packages/babel/__tests__/dynamic-import-noop.test.js
new file mode 100644
index 0000000..04f0452
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/dynamic-import-noop.test.js
@@ -0,0 +1,15 @@
+import { transformAsync } from '@babel/core';
+
+it('replaces dynamic imports with a noop', async () => {
+ const { code } = await transformAsync(
+ `import('./foo').then(foo => foo.init())`,
+ {
+ plugins: [require.resolve('../src/dynamic-import-noop')],
+ filename: 'source.js',
+ configFile: false,
+ babelrc: false,
+ }
+ );
+
+ expect(code).toMatchSnapshot();
+});
diff --git a/@linaria/packages/babel/__tests__/evaluators/__snapshots__/extractor.test.ts.snap b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/extractor.test.ts.snap
new file mode 100644
index 0000000..7ee2296
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/extractor.test.ts.snap
@@ -0,0 +1,237 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`keeps objects as is 1`] = `
+"{
+ const fill1 = (top = 0, left = 0, right = 0, bottom = 0) => ({
+ position: 'absolute',
+ top,
+ right,
+ bottom,
+ left
+ });
+
+ {
+ const fill2 = (top = 0, left = 0, right = 0, bottom = 0) => {
+ return {
+ position: 'absolute',
+ top,
+ right,
+ bottom,
+ left
+ };
+ };
+
+ {
+ exports.__linariaPreval = [fill1, fill2];
+ }
+ }
+}"
+`;
+
+exports[`keeps only code which is related to \`a\` 1`] = `
+"{
+ const {
+ whiteColor: color,
+ anotherColor
+ } = require('…');
+
+ {
+ const a = color || anotherColor;
+ {
+ exports.__linariaPreval = [a];
+ }
+ }
+}"
+`;
+
+exports[`keeps only code which is related to \`anotherColor\` 1`] = `
+"{
+ const {
+ whiteColor: color,
+ anotherColor
+ } = require('…');
+
+ {
+ exports.__linariaPreval = [anotherColor];
+ }
+}"
+`;
+
+exports[`keeps only code which is related to \`color\` 1`] = `
+"{
+ const {
+ whiteColor: color,
+ anotherColor
+ } = require('…');
+
+ {
+ exports.__linariaPreval = [color];
+ }
+}"
+`;
+
+exports[`removes all 1`] = `
+"{
+ exports.__linariaPreval = [];
+}"
+`;
+
+exports[`shakes assignment patterns 1`] = `
+"{
+ const [identifier = 1] = [2];
+ {
+ const [{ ...object
+ } = {}] = [{
+ a: 1,
+ b: 2
+ }];
+ {
+ const [[...array] = []] = [[1, 2, 3, 4]];
+ {
+ const obj = {
+ member: null
+ };
+ {
+ exports.__linariaPreval = [identifier, object, array, obj];
+ }
+ }
+ }
+ }
+}"
+`;
+
+exports[`shakes es5 exports 1`] = `
+"\\"use strict\\";
+
+Object.defineProperty(exports, \\"__esModule\\", {
+ value: true
+});
+exports.redColor = 'red';
+Object.defineProperty(exports, \\"blueColor\\", {
+ enumerable: true,
+ get: function get() {
+ return 'blue';
+ }
+});
+Object.defineProperty(exports, \\"greenColor\\", {
+ enumerable: true,
+ get: function get() {
+ return 'green';
+ }
+});"
+`;
+
+exports[`shakes exports 1`] = `
+"{
+ var _ = require(\\"\\\\u2026\\");
+
+ {
+ const a = _.whiteColor;
+ {
+ exports.__linariaPreval = [a];
+ }
+ }
+}"
+`;
+
+exports[`shakes imports 1`] = `
+"{
+ var _ = _interopRequireWildcard(require(\\"\\\\u2026\\"));
+
+ {
+ function _getRequireWildcardCache(nodeInterop) {
+ if (typeof WeakMap !== \\"function\\") return null;
+ var cacheBabelInterop = new WeakMap();
+ var cacheNodeInterop = new WeakMap();
+ return (_getRequireWildcardCache = function (nodeInterop) {
+ return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
+ })(nodeInterop);
+ }
+
+ {
+ function _interopRequireWildcard(obj, nodeInterop) {
+ if (!nodeInterop && obj && obj.__esModule) {
+ return obj;
+ }
+
+ if (obj === null || typeof obj !== \\"object\\" && typeof obj !== \\"function\\") {
+ return {
+ default: obj
+ };
+ }
+
+ var cache = _getRequireWildcardCache(nodeInterop);
+
+ if (cache && cache.has(obj)) {
+ return cache.get(obj);
+ }
+
+ var newObj = {};
+ var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
+
+ for (var key in obj) {
+ if (key !== \\"default\\" && Object.prototype.hasOwnProperty.call(obj, key)) {
+ var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
+
+ if (desc && (desc.get || desc.set)) {
+ Object.defineProperty(newObj, key, desc);
+ } else {
+ newObj[key] = obj[key];
+ }
+ }
+ }
+
+ newObj.default = obj;
+
+ if (cache) {
+ cache.set(obj, newObj);
+ }
+
+ return newObj;
+ }
+
+ {
+ exports.__linariaPreval = [_.whiteColor, _.default];
+ }
+ }
+ }
+}"
+`;
+
+exports[`shakes sequence expression 1`] = `
+"{
+ var _ = require(\\"\\\\u2026\\");
+
+ {
+ const color1 = () => 'blue';
+
+ {
+ let local = '';
+ {
+ const color2 = () => local;
+
+ {
+ exports.__linariaPreval = [color2];
+ }
+ }
+ }
+ }
+}"
+`;
+
+exports[`should keep member expression key 1`] = `
+"{
+ const key = 'blue';
+ {
+ const obj = {
+ blue: '#00F'
+ };
+ {
+ const blue = obj[key];
+ {
+ exports.__linariaPreval = [blue];
+ }
+ }
+ }
+}"
+`;
diff --git a/@linaria/packages/babel/__tests__/evaluators/__snapshots__/preeval.test.ts.snap b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/preeval.test.ts.snap
new file mode 100644
index 0000000..93660fa
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/preeval.test.ts.snap
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`handles locally named import 1`] = `
+"import { styled as custom } from '@linaria/react';
+const Component =
+/*linaria styled ci72cjv Component Component_ci72cjv*/
+custom.div\`\`;"
+`;
+
+exports[`hoists exports 1`] = `
+"\\"use strict\\";
+
+var _foo = require(\\"./foo\\");
+
+Object.defineProperty(exports, \\"__esModule\\", {
+ value: true
+});
+Object.defineProperty(exports, \\"foo\\", {
+ enumerable: true,
+ get: function get() {
+ return _foo.foo;
+ }
+});
+Object.defineProperty(exports, \\"bar\\", {
+ enumerable: true,
+ get: function get() {
+ return _foo.bar;
+ }
+});"
+`;
+
+exports[`preserves classNames 1`] = `
+"import { styled } from '@linaria/react';
+const Component =
+/*linaria styled ci72cjv Component Component_ci72cjv*/
+styled.div\`\`;"
+`;
+
+exports[`replaces class component 1`] = `
+"import React from 'react';
+
+function Component() {
+ return <></>;
+}"
+`;
+
+exports[`replaces constant 1`] = `
+"import React from 'react';
+const tag = <></>;
+
+const Component = props => tag;"
+`;
+
+exports[`replaces functional component 1`] = `
+"import React from 'react';
+
+const Component = () => {
+ return <></>;
+};"
+`;
diff --git a/@linaria/packages/babel/__tests__/evaluators/__snapshots__/shaker.test.ts.snap b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/shaker.test.ts.snap
new file mode 100644
index 0000000..92b9b9e
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/evaluators/__snapshots__/shaker.test.ts.snap
@@ -0,0 +1,213 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`keeps objects as is 1`] = `
+"\\"use strict\\";
+
+var fill1 = function fill1() {
+ var top = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
+ var left = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
+ var right = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
+ var bottom = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
+ return {
+ position: 'absolute',
+ top: top,
+ right: right,
+ bottom: bottom,
+ left: left
+ };
+};
+
+var fill2 = function fill2() {
+ var top = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
+ var left = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
+ var right = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
+ var bottom = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
+ return {
+ position: 'absolute',
+ top: top,
+ right: right,
+ bottom: bottom,
+ left: left
+ };
+};
+
+exports.__linariaPreval = [fill1, fill2];"
+`;
+
+exports[`keeps only code which is related to \`a\` 1`] = `
+"\\"use strict\\";
+
+var _require = require('…'),
+ color = _require.whiteColor,
+ anotherColor = _require.anotherColor;
+
+var a = color || anotherColor;
+color.green = '#0f0';
+exports.__linariaPreval = [a];"
+`;
+
+exports[`keeps only code which is related to \`anotherColor\` 1`] = `
+"\\"use strict\\";
+
+var _require = require('…'),
+ anotherColor = _require.anotherColor;
+
+exports.__linariaPreval = [anotherColor];"
+`;
+
+exports[`keeps only code which is related to \`color\` 1`] = `
+"\\"use strict\\";
+
+var _require = require('…'),
+ color = _require.whiteColor;
+
+color.green = '#0f0';
+exports.__linariaPreval = [color];"
+`;
+
+exports[`keeps only the last assignment of each exported variable 1`] = `
+"\\"use strict\\";
+
+var bar = function bar() {
+ return 'hello world';
+};
+
+exports.bar = bar;
+var foo = exports.bar();
+exports.__linariaPreval = [foo];"
+`;
+
+exports[`keeps reused exports 1`] = `
+"\\"use strict\\";
+
+var bar = function bar() {
+ return 'hello world';
+};
+
+exports.bar = bar;
+var foo = exports.bar();
+exports.__linariaPreval = [foo];"
+`;
+
+exports[`removes all 1`] = `
+"\\"use strict\\";
+
+exports.__linariaPreval = [];"
+`;
+
+exports[`shakes assignment patterns 1`] = `
+"\\"use strict\\";
+
+var _interopRequireDefault = require(\\"@babel/runtime/helpers/interopRequireDefault\\");
+
+var _toArray2 = _interopRequireDefault(require(\\"@babel/runtime/helpers/toArray\\"));
+
+var _extends2 = _interopRequireDefault(require(\\"@babel/runtime/helpers/extends\\"));
+
+var _ = 2,
+ identifier = _ === void 0 ? 1 : _;
+var _a$b = {
+ a: 1,
+ b: 2
+};
+_a$b = _a$b === void 0 ? {} : _a$b;
+var object = (_extends2.default)({}, _a$b);
+var _ref = [1, 2, 3, 4];
+_ref = _ref === void 0 ? [] : _ref;
+
+var _ref2 = (_toArray2.default)(_ref),
+ array = _ref2.slice(0);
+
+var obj = {
+ member: null
+};
+var _2 = 1;
+obj.member = _2 === void 0 ? 42 : _2;
+exports.__linariaPreval = [identifier, object, array, obj];"
+`;
+
+exports[`shakes es5 exports 1`] = `
+"\\"use strict\\";
+
+Object.defineProperty(exports, \\"__esModule\\", {
+ value: true
+});
+exports.redColor = 'red';
+exports['yellowColor'] = 'yellow';
+Object.defineProperty(exports, \\"greenColor\\", {
+ enumerable: true,
+ get: function get() {
+ return 'green';
+ }
+});"
+`;
+
+exports[`shakes exports 1`] = `
+"\\"use strict\\";
+
+var _ = require(\\"\\\\u2026\\");
+
+Object.defineProperty(exports, \\"__esModule\\", {
+ value: true
+});
+var a = _.whiteColor;
+exports.__linariaPreval = [a];"
+`;
+
+exports[`shakes for-in statements 1`] = `
+"\\"use strict\\";
+
+var obj1 = {
+ a: 1,
+ b: 2
+};
+var obj2 = {};
+
+for (var key in obj1) {
+ obj2[key] = obj1[key];
+}
+
+exports.__linariaPreval = [obj2];"
+`;
+
+exports[`shakes imports 1`] = `
+"\\"use strict\\";
+
+var _typeof = require(\\"@babel/runtime/helpers/typeof\\");
+
+Object.defineProperty(exports, \\"__esModule\\", {
+ value: true
+});
+
+var _ = _interopRequireWildcard(require(\\"\\\\u2026\\"));
+
+function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== \\"function\\") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
+
+function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== \\"object\\" && typeof obj !== \\"function\\") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== \\"default\\" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
+
+exports.__linariaPreval = [_.whiteColor, _.default];"
+`;
+
+exports[`shakes sequence expression 1`] = `
+"\\"use strict\\";
+
+var color1 = (function () {
+ return 'blue';
+});
+var local = '';
+var color2 = (local = color1(), function () {
+ return local;
+});
+exports.__linariaPreval = [color2];"
+`;
+
+exports[`should keep member expression key 1`] = `
+"\\"use strict\\";
+
+var key = 'blue';
+var obj = {
+ blue: '#00F'
+};
+var blue = obj[key];
+exports.__linariaPreval = [blue];"
+`;
diff --git a/@linaria/packages/babel/__tests__/evaluators/extractor.test.ts b/@linaria/packages/babel/__tests__/evaluators/extractor.test.ts
new file mode 100644
index 0000000..b764a25
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/evaluators/extractor.test.ts
@@ -0,0 +1,214 @@
+import path from 'path';
+import dedent from 'dedent';
+import type { TransformOptions } from '@babel/core';
+import exctract from '../../../extractor/src';
+
+function getFileName() {
+ return path.resolve(__dirname, `../__fixtures__/test.js`);
+}
+
+function _shake(opts?: TransformOptions, only: string[] = ['__linariaPreval']) {
+ return (
+ literal: TemplateStringsArray,
+ ...placeholders: string[]
+ ): [string, Map<string, string[]>] => {
+ const code = dedent(literal, ...placeholders);
+ const [shaken, deps] = exctract(
+ getFileName(),
+ {
+ babelOptions: opts || {},
+ displayName: true,
+ evaluate: true,
+ rules: [
+ {
+ action: require('../../../extractor/src').default,
+ },
+ {
+ test: /\/node_modules\//,
+ action: 'ignore',
+ },
+ ],
+ },
+ code,
+ only
+ );
+
+ return [shaken, deps!];
+ };
+}
+
+it('removes all', () => {
+ const [shaken] = _shake()`
+ const { whiteColor: color, anotherColor } = require('…');
+ const a = color || anotherColor;
+ color.green = '#0f0';
+
+ exports.__linariaPreval = [];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('keeps only code which is related to `color`', () => {
+ const [shaken] = _shake()`
+ const { whiteColor: color, anotherColor } = require('…');
+ const wrap = '';
+ const a = color || anotherColor;
+ color.green = '#0f0';
+ module.exports = { color, anotherColor };
+ exports.__linariaPreval = [color];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('keeps only code which is related to `anotherColor`', () => {
+ const [shaken] = _shake()`
+ const { whiteColor: color, anotherColor } = require('…');
+ const a = color || anotherColor;
+ color.green = '#0f0';
+ exports.__linariaPreval = [anotherColor];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('keeps only code which is related to `a`', () => {
+ const [shaken] = _shake()`
+ const { whiteColor: color, anotherColor } = require('…');
+ const a = color || anotherColor;
+ color.green = '#0f0';
+ exports.__linariaPreval = [a];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('shakes imports', () => {
+ const [shaken] = _shake()`
+ import { unrelatedImport } from '…';
+ import { whiteColor as color, anotherColor } from '…';
+ import defaultColor from '…';
+ import anotherDefaultColor from '…';
+ import '…';
+ require('…');
+ export default color;
+ exports.__linariaPreval = [color, defaultColor];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('should keep member expression key', () => {
+ const [shaken] = _shake()`
+ const key = 'blue';
+ const obj = { blue: '#00F' };
+ const blue = obj[key];
+ exports.__linariaPreval = [blue];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('shakes exports', () => {
+ const [shaken] = _shake()`
+ import { whiteColor as color, anotherColor } from '…';
+ export const a = color;
+ export { redColor } from "…";
+ export { anotherColor };
+ exports.__linariaPreval = [a];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('shakes es5 exports', () => {
+ const [shaken] = _shake(undefined, ['redColor', 'greenColor'])`
+ "use strict";
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+ exports.redColor = 'red';
+ Object.defineProperty(exports, "blueColor", {
+ enumerable: true,
+ get: function get() {
+ return 'blue';
+ }
+ });
+ Object.defineProperty(exports, "greenColor", {
+ enumerable: true,
+ get: function get() {
+ return 'green';
+ }
+ });
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+// TODO: this test will be disabled until the shaker is fully implemented
+// eslint-disable-next-line jest/no-disabled-tests
+it.skip('should throw away any side effects', () => {
+ const [shaken] = _shake()`
+ const objects = { key: { fontSize: 12 } };
+ const foo = (k) => {
+ const obj = objects[k];
+ console.log('side effect');
+ return obj;
+ };
+ exports.__linariaPreval = [foo];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('keeps objects as is', () => {
+ const [shaken] = _shake()`
+ const fill1 = (top = 0, left = 0, right = 0, bottom = 0) => ({
+ position: 'absolute',
+ top,
+ right,
+ bottom,
+ left,
+ });
+
+ const fill2 = (top = 0, left = 0, right = 0, bottom = 0) => {
+ return {
+ position: 'absolute',
+ top,
+ right,
+ bottom,
+ left,
+ };
+ };
+
+ exports.__linariaPreval = [fill1, fill2];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('shakes sequence expression', () => {
+ const [shaken] = _shake()`
+ import { external } from '…';
+ const color1 = (external, () => 'blue');
+ let local = '';
+ const color2 = (local = color1(), () => local);
+ exports.__linariaPreval = [color2];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('shakes assignment patterns', () => {
+ const [shaken] = _shake()`
+ const [identifier = 1] = [2];
+ const [{...object} = {}] = [{ a: 1, b: 2 }];
+ const [[...array] = []] = [[1,2,3,4]];
+ const obj = { member: null };
+ ([obj.member = 42] = [1]);
+ exports.__linariaPreval = [identifier, object, array, obj];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
diff --git a/@linaria/packages/babel/__tests__/evaluators/preeval.test.ts b/@linaria/packages/babel/__tests__/evaluators/preeval.test.ts
new file mode 100644
index 0000000..307abd7
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/evaluators/preeval.test.ts
@@ -0,0 +1,121 @@
+import { join } from 'path';
+import { transformAsync } from '@babel/core';
+import dedent from 'dedent';
+import serializer from '../../__utils__/linaria-snapshot-serializer';
+import type { StrictOptions } from '../../src';
+
+expect.addSnapshotSerializer(serializer);
+
+const options: Partial<StrictOptions> = {
+ displayName: true,
+ evaluate: true,
+};
+
+const transpile = async (input: string) =>
+ (await transformAsync(input, {
+ babelrc: false,
+ presets: [[require.resolve('@linaria/preeval'), options]],
+ plugins: [
+ '@babel/plugin-proposal-class-properties',
+ '@babel/plugin-syntax-jsx',
+ ],
+ filename: join(__dirname, 'app/index.js'),
+ configFile: false,
+ }))!;
+
+it('preserves classNames', async () => {
+ const { code } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const Component = styled.div\`\`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+});
+
+it('handles locally named import', async () => {
+ const { code } = await transpile(
+ dedent`
+ import { styled as custom } from '@linaria/react';
+
+ const Component = custom.div\`\`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+});
+
+it('replaces functional component', async () => {
+ const div = '<div>{props.children}</div>';
+ const { code } = await transpile(
+ dedent`
+ import React from 'react';
+
+ const Component = (props) => ${div};
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+});
+
+it('replaces class component', async () => {
+ const div = '<div>{props.children}</div>';
+ const { code } = await transpile(
+ dedent`
+ import React from 'react';
+
+ class Component extends React.PureComponent {
+ render() {
+ return ${div};
+ }
+ }
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+});
+
+it('replaces constant', async () => {
+ const div = '<div>{props.children}</div>';
+ const { code } = await transpile(
+ dedent`
+ import React from 'react';
+
+ const tag = ${div};
+
+ const Component = (props) => tag;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+});
+
+it('hoists exports', async () => {
+ const { code } = await transpile(
+ dedent`
+ "use strict";
+
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+ Object.defineProperty(exports, "foo", {
+ enumerable: true,
+ get: function get() {
+ return _foo.foo;
+ }
+ });
+ Object.defineProperty(exports, "bar", {
+ enumerable: true,
+ get: function get() {
+ return _foo.bar;
+ }
+ });
+
+ var _foo = require("./foo");
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+});
diff --git a/@linaria/packages/babel/__tests__/evaluators/shaker.test.ts b/@linaria/packages/babel/__tests__/evaluators/shaker.test.ts
new file mode 100644
index 0000000..1208065
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/evaluators/shaker.test.ts
@@ -0,0 +1,259 @@
+import path from 'path';
+import dedent from 'dedent';
+import type { TransformOptions } from '@babel/core';
+import shake from '../../../shaker/src';
+
+function getFileName() {
+ return path.resolve(__dirname, `../__fixtures__/test.js`);
+}
+
+function _shake(opts?: TransformOptions, only: string[] = ['__linariaPreval']) {
+ return (
+ literal: TemplateStringsArray,
+ ...placeholders: string[]
+ ): [string, Map<string, string[]>] => {
+ const code = dedent(literal, ...placeholders);
+ const [shaken, deps] = shake(
+ getFileName(),
+ {
+ babelOptions: opts || {},
+ displayName: true,
+ evaluate: true,
+ rules: [
+ {
+ action: require('../../../extractor/src').default,
+ },
+ {
+ test: /\/node_modules\//,
+ action: 'ignore',
+ },
+ ],
+ },
+ code,
+ only
+ );
+
+ return [shaken, deps!];
+ };
+}
+
+it('removes all', () => {
+ const [shaken] = _shake()`
+ const { whiteColor: color, anotherColor } = require('…');
+ const a = color || anotherColor;
+ color.green = '#0f0';
+
+ exports.__linariaPreval = [];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('keeps only code which is related to `color`', () => {
+ const [shaken] = _shake()`
+ const { whiteColor: color, anotherColor } = require('…');
+ const wrap = '';
+ const a = color || anotherColor;
+ color.green = '#0f0';
+ module.exports = { color, anotherColor };
+ exports.__linariaPreval = [color];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('keeps only code which is related to `anotherColor`', () => {
+ const [shaken] = _shake()`
+ const { whiteColor: color, anotherColor } = require('…');
+ const a = color || anotherColor;
+ color.green = '#0f0';
+ exports.__linariaPreval = [anotherColor];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('keeps only code which is related to `a`', () => {
+ const [shaken] = _shake()`
+ const { whiteColor: color, anotherColor } = require('…');
+ const a = color || anotherColor;
+ color.green = '#0f0';
+ exports.__linariaPreval = [a];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('shakes imports', () => {
+ const [shaken] = _shake()`
+ import { unrelatedImport } from '…';
+ import { whiteColor as color, anotherColor } from '…';
+ import defaultColor from '…';
+ import anotherDefaultColor from '…';
+ import '…';
+ require('…');
+ export default color;
+ exports.__linariaPreval = [color, defaultColor];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('should keep member expression key', () => {
+ const [shaken] = _shake()`
+ const key = 'blue';
+ const obj = { blue: '#00F' };
+ const blue = obj[key];
+ exports.__linariaPreval = [blue];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('shakes exports', () => {
+ const [shaken] = _shake()`
+ import { whiteColor as color, anotherColor } from '…';
+ export const a = color;
+ export { redColor } from "…";
+ export { anotherColor };
+ exports.__linariaPreval = [a];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('shakes es5 exports', () => {
+ const [shaken] = _shake(undefined, ['redColor', 'greenColor', 'yellowColor'])`
+ "use strict";
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+ exports.redColor = 'red';
+ exports['yellowColor'] = 'yellow';
+ exports['pinkColor'] = 'pink';
+ Object.defineProperty(exports, "blueColor", {
+ enumerable: true,
+ get: function get() {
+ return 'blue';
+ }
+ });
+ Object.defineProperty(exports, "greenColor", {
+ enumerable: true,
+ get: function get() {
+ return 'green';
+ }
+ });
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+// TODO: this test will be disabled until the shaker is fully implemented
+// eslint-disable-next-line jest/no-disabled-tests
+it.skip('should throw away any side effects', () => {
+ const [shaken] = _shake()`
+ const objects = { key: { fontSize: 12 } };
+ const foo = (k) => {
+ const obj = objects[k];
+ console.log('side effect');
+ return obj;
+ };
+ exports.__linariaPreval = [foo];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('keeps objects as is', () => {
+ const [shaken] = _shake()`
+ const fill1 = (top = 0, left = 0, right = 0, bottom = 0) => ({
+ position: 'absolute',
+ top,
+ right,
+ bottom,
+ left,
+ });
+
+ const fill2 = (top = 0, left = 0, right = 0, bottom = 0) => {
+ return {
+ position: 'absolute',
+ top,
+ right,
+ bottom,
+ left,
+ };
+ };
+
+ exports.__linariaPreval = [fill1, fill2];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('shakes sequence expression', () => {
+ const [shaken] = _shake()`
+ import { external } from '…';
+ const color1 = (external, () => 'blue');
+ let local = '';
+ const color2 = (local = color1(), () => local);
+ exports.__linariaPreval = [color2];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('shakes assignment patterns', () => {
+ const [shaken] = _shake()`
+ const [identifier = 1] = [2];
+ const [{...object} = {}] = [{ a: 1, b: 2 }];
+ const [[...array] = []] = [[1,2,3,4]];
+ const obj = { member: null };
+ ([obj.member = 42] = [1]);
+ exports.__linariaPreval = [identifier, object, array, obj];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('shakes for-in statements', () => {
+ const [shaken] = _shake()`
+ const obj1 = { a: 1, b: 2 };
+ const obj2 = {};
+ for (const key in obj1) {
+ obj2[key] = obj1[key];
+ }
+ exports.__linariaPreval = [obj2];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('keeps reused exports', () => {
+ const [shaken] = _shake()`
+ const bar = function() {
+ return 'hello world';
+ };
+ exports.bar = bar;
+
+ const foo = exports.bar();
+ exports.__linariaPreval = [foo];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
+
+it('keeps only the last assignment of each exported variable', () => {
+ const [shaken] = _shake()`
+ const bar = function() {
+ return 'hello world';
+ };
+
+ exports.bar = "bar";
+ exports.bar = bar;
+
+ const foo = exports.bar();
+ exports.__linariaPreval = [foo];
+ `;
+
+ expect(shaken).toMatchSnapshot();
+});
diff --git a/@linaria/packages/babel/__tests__/module.test.ts b/@linaria/packages/babel/__tests__/module.test.ts
new file mode 100644
index 0000000..1012b93
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/module.test.ts
@@ -0,0 +1,320 @@
+import path from 'path';
+import dedent from 'dedent';
+import * as babel from '@babel/core';
+import { Module } from '../src';
+import type { Evaluator, StrictOptions } from '../src';
+
+beforeEach(() => Module.invalidate());
+
+const evaluator: Evaluator = (filename, options, text) => {
+ const { code } = babel.transformSync(text, {
+ filename: filename,
+ })!;
+ return [code!, null];
+};
+
+function getFileName() {
+ return path.resolve(__dirname, `../__fixtures__/test.js`);
+}
+
+const options: StrictOptions = {
+ displayName: false,
+ evaluate: true,
+ rules: [
+ {
+ action: evaluator,
+ },
+ {
+ test: /\/node_modules\//,
+ action: 'ignore',
+ },
+ ],
+ babelOptions: {},
+};
+
+beforeEach(() => Module.invalidateEvalCache());
+
+it('creates module for JS files', () => {
+ const filename = '/foo/bar/test.js';
+ const mod = new Module(filename, options);
+
+ mod.evaluate('module.exports = () => 42');
+
+ expect(mod.exports()).toBe(42);
+ expect(mod.id).toBe(filename);
+ expect(mod.filename).toBe(filename);
+});
+
+it('requires JS files', () => {
+ const mod = new Module(getFileName(), options);
+
+ mod.evaluate(dedent`
+ const answer = require('./sample-script');
+
+ module.exports = 'The answer is ' + answer;
+ `);
+
+ expect(mod.exports).toBe('The answer is 42');
+});
+
+it('requires JSON files', () => {
+ const mod = new Module(getFileName(), options);
+
+ mod.evaluate(dedent`
+ const data = require('./sample-data.json');
+
+ module.exports = 'Our saviour, ' + data.name;
+ `);
+
+ expect(mod.exports).toBe('Our saviour, Luke Skywalker');
+});
+
+it('imports JS files', () => {
+ const mod = new Module(getFileName(), options);
+
+ mod.evaluate(dedent`
+ import answer from './sample-script';
+
+ export const result = 'The answer is ' + answer;
+ `);
+
+ expect(mod.exports.result).toBe('The answer is 42');
+});
+
+it('imports TypeScript files', () => {
+ const mod = new Module(
+ path.resolve(__dirname, '../__fixtures__/test.ts'),
+ options
+ );
+
+ mod.evaluate(dedent`
+ import answer from './sample-typescript';
+
+ export const result = 'The answer is ' + answer;
+ `);
+
+ expect(mod.exports.result).toBe('The answer is 27');
+});
+
+it('imports JSON files', () => {
+ const mod = new Module(getFileName(), options);
+
+ mod.evaluate(dedent`
+ import data from './sample-data.json';
+
+ const result = 'Our saviour, ' + data.name;
+
+ export default result;
+ `);
+
+ expect(mod.exports.default).toBe('Our saviour, Luke Skywalker');
+});
+
+it('returns module from the cache', () => {
+ /* eslint-disable no-self-compare */
+
+ const filename = getFileName();
+ const mod = new Module(filename, options);
+ const id = './sample-data.json';
+
+ expect(mod.require(id) === mod.require(id)).toBe(true);
+
+ expect(
+ new Module(filename, options).require(id) ===
+ new Module(filename, options).require(id)
+ ).toBe(true);
+});
+
+it('clears modules from the cache', () => {
+ const filename = getFileName();
+ const id = './sample-data.json';
+
+ const result = new Module(filename, options).require(id);
+
+ expect(result === new Module(filename, options).require(id)).toBe(true);
+
+ Module.invalidate();
+
+ expect(result === new Module(filename, options).require(id)).toBe(false);
+});
+
+it('exports the path for non JS/JSON files', () => {
+ const mod = new Module(getFileName(), options);
+
+ expect(mod.require('./sample-asset.png')).toBe('./sample-asset.png');
+});
+
+it('returns module when requiring mocked builtin node modules', () => {
+ const mod = new Module(getFileName(), options);
+
+ expect(mod.require('path')).toBe(require('path'));
+});
+
+it('returns null when requiring empty builtin node modules', () => {
+ const mod = new Module(getFileName(), options);
+
+ expect(mod.require('fs')).toBe(null);
+});
+
+it('throws when requiring unmocked builtin node modules', () => {
+ const mod = new Module(getFileName(), options);
+
+ expect(() => mod.require('perf_hooks')).toThrow(
+ 'Unable to import "perf_hooks". Importing Node builtins is not supported in the sandbox.'
+ );
+});
+
+it('has access to the global object', () => {
+ const mod = new Module(getFileName(), options);
+
+ expect(() =>
+ mod.evaluate(dedent`
+ new global.Set();
+ `)
+ ).not.toThrow();
+});
+
+it("doesn't have access to the process object", () => {
+ const mod = new Module(getFileName(), options);
+
+ expect(() =>
+ mod.evaluate(dedent`
+ module.exports = process.abort();
+ `)
+ ).toThrow('process.abort is not a function');
+});
+
+it('has access to NODE_ENV', () => {
+ const mod = new Module(getFileName(), options);
+
+ mod.evaluate(dedent`
+ module.exports = process.env.NODE_ENV;
+ `);
+
+ expect(mod.exports).toBe(process.env.NODE_ENV);
+});
+
+it('has require.resolve available', () => {
+ const mod = new Module(getFileName(), options);
+
+ mod.evaluate(dedent`
+ module.exports = require.resolve('./sample-script');
+ `);
+
+ expect(mod.exports).toBe(
+ path.resolve(path.dirname(mod.filename), 'sample-script.js')
+ );
+});
+
+it('has require.ensure available', () => {
+ const mod = new Module(getFileName(), options);
+
+ expect(() =>
+ mod.evaluate(dedent`
+ require.ensure(['./sample-script']);
+ `)
+ ).not.toThrow();
+});
+
+it('has __filename available', () => {
+ const mod = new Module(getFileName(), options);
+
+ mod.evaluate(dedent`
+ module.exports = __filename;
+ `);
+
+ expect(mod.exports).toBe(mod.filename);
+});
+
+it('has __dirname available', () => {
+ const mod = new Module(getFileName(), options);
+
+ mod.evaluate(dedent`
+ module.exports = __dirname;
+ `);
+
+ expect(mod.exports).toBe(path.dirname(mod.filename));
+});
+
+it('has setTimeout, clearTimeout available', () => {
+ const mod = new Module(getFileName(), options);
+
+ expect(() =>
+ mod.evaluate(dedent`
+ const x = setTimeout(() => {
+ console.log('test');
+ },0);
+
+ clearTimeout(x);
+ `)
+ ).not.toThrow();
+});
+
+it('has setInterval, clearInterval available', () => {
+ const mod = new Module(getFileName(), options);
+
+ expect(() =>
+ mod.evaluate(dedent`
+ const x = setInterval(() => {
+ console.log('test');
+ }, 1000);
+
+ clearInterval(x);
+ `)
+ ).not.toThrow();
+});
+
+it('has setImmediate, clearImmediate available', () => {
+ const mod = new Module(getFileName(), options);
+
+ expect(() =>
+ mod.evaluate(dedent`
+ const x = setImmediate(() => {
+ console.log('test');
+ });
+
+ clearImmediate(x);
+ `)
+ ).not.toThrow();
+});
+
+it('has global objects available without referencing global', () => {
+ const mod = new Module(getFileName(), options);
+
+ expect(() =>
+ mod.evaluate(dedent`
+ const x = new Set();
+ `)
+ ).not.toThrow();
+});
+
+it('changes resolve behaviour on overriding _resolveFilename', () => {
+ const originalResolveFilename = Module._resolveFilename;
+
+ Module._resolveFilename = (id) => (id === 'foo' ? 'bar' : id);
+
+ const mod = new Module(getFileName(), options);
+
+ mod.evaluate(dedent`
+ module.exports = [
+ require.resolve('foo'),
+ require.resolve('test'),
+ ];
+ `);
+
+ // Restore old behavior
+ Module._resolveFilename = originalResolveFilename;
+
+ expect(mod.exports).toEqual(['bar', 'test']);
+});
+
+it('correctly processes export declarations in strict mode', () => {
+ const filename = '/foo/bar/test.js';
+ const mod = new Module(filename, options);
+
+ mod.evaluate('"use strict"; exports = module.exports = () => 42');
+
+ expect(mod.exports()).toBe(42);
+ expect(mod.id).toBe(filename);
+ expect(mod.filename).toBe(filename);
+});
diff --git a/@linaria/packages/babel/__tests__/transform.test.ts b/@linaria/packages/babel/__tests__/transform.test.ts
new file mode 100644
index 0000000..c830ad9
--- /dev/null
+++ b/@linaria/packages/babel/__tests__/transform.test.ts
@@ -0,0 +1,237 @@
+/* eslint-disable no-template-curly-in-string */
+
+import path from 'path';
+import dedent from 'dedent';
+import transform, { transformUrl } from '../src/transform';
+import evaluator from '../../extractor/src';
+
+const outputFilename = './.linaria-cache/test.css';
+
+const rules = [
+ {
+ test: () => true,
+ action: evaluator,
+ },
+];
+
+describe('transformUrl', () => {
+ type TransformUrlArgs = Parameters<typeof transformUrl>;
+ const dataset: Record<string, TransformUrlArgs> = {
+ '../assets/test.jpg': [
+ './assets/test.jpg',
+ './.linaria-cache/test.css',
+ './test.js',
+ ],
+ '../a/b/test.jpg': [
+ '../a/b/test.jpg',
+ './.linaria-cache/test.css',
+ './a/test.js',
+ ],
+ };
+
+ it('should work with posix paths', () => {
+ for (const result of Object.keys(dataset)) {
+ expect(transformUrl(...dataset[result])).toBe(result);
+ }
+ });
+
+ it('should work with win32 paths', () => {
+ const toWin32 = (p: string) => p.split(path.posix.sep).join(path.win32.sep);
+ const win32Dataset = Object.keys(dataset).reduce(
+ (acc, key) => ({
+ ...acc,
+ [key]: [
+ dataset[key][0],
+ toWin32(dataset[key][1]),
+ toWin32(dataset[key][2]),
+ path.win32,
+ ] as TransformUrlArgs,
+ }),
+ {} as Record<string, TransformUrlArgs>
+ );
+
+ for (const result of Object.keys(win32Dataset)) {
+ expect(transformUrl(...win32Dataset[result])).toBe(result);
+ }
+ });
+});
+
+it('rewrites a relative path in url() declarations', async () => {
+ const { cssText } = await transform(
+ dedent`
+ import { css } from '@linaria/core';
+
+ export const title = css\`
+ background-image: url(./assets/test.jpg);
+ background-image: url("./assets/test.jpg");
+ background-image: url('./assets/test.jpg');
+ \`;
+ `,
+ {
+ filename: './test.js',
+ outputFilename: './.linaria-cache/test.css',
+ pluginOptions: {
+ rules,
+ },
+ }
+ );
+
+ expect(cssText).toMatchSnapshot();
+});
+
+it('rewrites multiple relative paths in url() declarations', async () => {
+ const { cssText } = await transform(
+ dedent`
+ import { css } from '@linaria/core';
+
+ export const title = css\`
+ @font-face {
+ font-family: Test;
+ src: url(./assets/font.woff2) format("woff2"), url(./assets/font.woff) format("woff");
+ }
+ \`;
+ `,
+ {
+ filename: './test.js',
+ outputFilename,
+ pluginOptions: {
+ rules,
+ },
+ }
+ );
+
+ expect(cssText).toMatchSnapshot();
+});
+
+it("doesn't rewrite an absolute path in url() declarations", async () => {
+ const { cssText } = await transform(
+ dedent`
+ import { css } from '@linaria/core';
+
+ export const title = css\`
+ background-image: url(/assets/test.jpg);
+ \`;
+ `,
+ {
+ filename: './test.js',
+ outputFilename,
+ pluginOptions: {
+ rules,
+ },
+ }
+ );
+
+ expect(cssText).toMatchSnapshot();
+});
+
+it('respects passed babel options', async () => {
+ expect.assertions(2);
+
+ expect(() =>
+ transform(
+ dedent`
+ import { css } from '@linaria/core';
+
+ export const error = <jsx />;
+ `,
+ {
+ filename: './test.js',
+ outputFilename,
+ pluginOptions: {
+ rules,
+ babelOptions: {
+ babelrc: false,
+ configFile: false,
+ presets: [['@babel/preset-env', { loose: true }]],
+ },
+ },
+ }
+ )
+ ).toThrow(
+ /Support for the experimental syntax 'jsx' isn't currently enabled/
+ );
+
+ expect(() =>
+ transform(
+ dedent`
+ import { css } from '@linaria/core';
+
+ export const error = <jsx />;
+ export const title = css\`
+ background-image: url(/assets/test.jpg);
+ \`;
+ `,
+ {
+ filename: './test.js',
+ outputFilename,
+ pluginOptions: {
+ rules,
+ babelOptions: {
+ babelrc: false,
+ configFile: false,
+ presets: [
+ ['@babel/preset-env', { loose: true }],
+ '@babel/preset-react',
+ ],
+ },
+ },
+ }
+ )
+ ).not.toThrow('Unexpected token');
+});
+
+it("doesn't throw due to duplicate preset", async () => {
+ expect.assertions(1);
+
+ expect(() =>
+ transform(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const Title = styled.h1\` color: blue; \`;
+
+ const Article = styled.article\`
+ ${'${Title}'} {
+ font-size: 16px;
+ }
+ \`;
+ `,
+ {
+ filename: './test.js',
+ outputFilename,
+ pluginOptions: {
+ rules,
+ babelOptions: {
+ babelrc: false,
+ configFile: false,
+ presets: [require.resolve('../src')],
+ plugins: [
+ require.resolve('@babel/plugin-transform-modules-commonjs'),
+ ],
+ },
+ },
+ }
+ )
+ ).not.toThrow('Duplicate plugin/preset detected');
+});
+
+it('should return transformed code even when file only contains unused linaria code', async () => {
+ const { code } = await transform(
+ dedent`
+ import { css } from '@linaria/core';
+
+ const title = css\`
+ color: red;
+ \`;
+ `,
+ {
+ filename: './test.js',
+ outputFilename,
+ pluginOptions: {
+ rules,
+ },
+ }
+ );
+
+ expect(code).not.toContain('css`');
+});
diff --git a/@linaria/packages/babel/__utils__/linaria-snapshot-serializer.ts b/@linaria/packages/babel/__utils__/linaria-snapshot-serializer.ts
new file mode 100644
index 0000000..1e65cd3
--- /dev/null
+++ b/@linaria/packages/babel/__utils__/linaria-snapshot-serializer.ts
@@ -0,0 +1,26 @@
+import type { LinariaMetadata } from '../src';
+
+type Serializer<T> = {
+ test: (value: any) => value is T;
+ print: (value: T) => string;
+};
+
+const withLinariaMetadata = (
+ value: any
+): value is { linaria: LinariaMetadata } =>
+ value && typeof value.linaria === 'object';
+
+export default {
+ test: withLinariaMetadata,
+ print: ({ linaria }) => `
+CSS:
+
+${Object.keys(linaria.rules)
+ .map((selector) => `${selector} {${linaria.rules[selector].cssText}}`)
+ .join('\n')}
+
+Dependencies: ${
+ linaria.dependencies.length ? linaria.dependencies.join(', ') : 'NA'
+ }
+`,
+} as Serializer<{ linaria: LinariaMetadata }>;
diff --git a/@linaria/packages/babel/__utils__/strategy-tester.ts b/@linaria/packages/babel/__utils__/strategy-tester.ts
new file mode 100644
index 0000000..168b491
--- /dev/null
+++ b/@linaria/packages/babel/__utils__/strategy-tester.ts
@@ -0,0 +1,976 @@
+/* eslint-env jest */
+/* eslint-disable import/no-extraneous-dependencies */
+import { join, resolve } from 'path';
+import * as babel from '@babel/core';
+import dedent from 'dedent';
+import stripAnsi from 'strip-ansi';
+
+import { Module } from '../src';
+import type { Evaluator, StrictOptions } from '../src';
+import serializer from './linaria-snapshot-serializer';
+
+expect.addSnapshotSerializer(serializer);
+async function transformAsync(
+ code: string,
+ opts?: babel.TransformOptions
+): Promise<babel.BabelFileResult> {
+ return (await babel.transformAsync(code, opts))!;
+}
+
+async function transformFileAsync(
+ filename: string,
+ opts?: babel.TransformOptions
+): Promise<babel.BabelFileResult> {
+ return (await babel.transformFileAsync(filename, opts))!;
+}
+
+type TranspileFn = (
+ input: string,
+ conf?: (original: babel.TransformOptions) => babel.TransformOptions
+) => Promise<babel.BabelFileResult>;
+
+// eslint-disable-next-line jest/no-export
+export function run(
+ dirname: string,
+ evaluator: Evaluator,
+ strategyDependentTests: (transpile: TranspileFn) => void = () => {}
+): void {
+ const babelrc: babel.TransformOptions = {
+ babelrc: false,
+ configFile: false,
+ plugins: [],
+ presets: [
+ [
+ require.resolve('../src'),
+ {
+ displayName: true,
+ evaluate: true,
+ rules: [
+ {
+ action: evaluator,
+ },
+ {
+ test: /\/node_modules\/(?!@linaria)/,
+ action: 'ignore',
+ },
+ {
+ test: /\/@linaria\/(?:core|react)\/\w+\.ts/,
+ action: (
+ filename: string,
+ options: StrictOptions,
+ text: string
+ ) => {
+ const { code } = babel.transformSync(text, {
+ filename,
+ })!;
+ return [code, new Map<string, string[]>()];
+ },
+ },
+ ],
+ },
+ ],
+ ],
+ };
+
+ const transpile: TranspileFn = async (
+ input: string,
+ conf: (original: typeof babelrc) => typeof babelrc = (i) => i
+ ) => {
+ const { code, metadata } = await transformAsync(input, {
+ filename: join(dirname, 'source.js'),
+ ...conf(babelrc),
+ });
+ // The slug will be machine specific, so replace it with a consistent one
+ return {
+ metadata,
+ code,
+ };
+ };
+
+ beforeEach(() => Module.invalidateEvalCache());
+
+ it('evaluates identifier in scope', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const answer = 42;
+ const foo = () => answer;
+ const days = foo() + ' days';
+
+ export const Title = styled.h1\`
+ &:before {
+ content: "${'${days}'}"
+ }
+ \`;
+ `
+ );
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('hoistable identifiers', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ {
+ var days = 42;
+ }
+
+ export const Title = styled.h1\`
+ &:before {
+ content: "${'${days}'}"
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('non-hoistable identifiers', async () => {
+ expect.assertions(1);
+
+ try {
+ await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ {
+ const days = 42;
+ }
+
+ export const Title = styled.h1\`
+ &:before {
+ content: "${'${days}'}"
+ }
+ \`;
+ `
+ );
+ } catch (e) {
+ expect(
+ stripAnsi(e.message.replace(dirname, '<<DIRNAME>>'))
+ ).toMatchSnapshot();
+ }
+ });
+
+ it('evaluates babel helpers', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ function copyAndExtend(a, b) {
+ return { ...a, ...b };
+ }
+
+ const obj = copyAndExtend({ a: 1 }, { a: 2 });
+
+ export const Title = styled.h1\`
+ &:before {
+ content: "${'${obj.a}'}"
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('evaluates imported typescript enums', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+ import { Colors } from '@linaria/babel-preset/__fixtures__/enums';
+
+ export const Title = styled.h1\`
+ color: ${'${Colors.BLUE}'};
+ \`;
+ `,
+ (config) => ({
+ ...config,
+ presets: ['@babel/preset-typescript', ...(config.presets ?? [])],
+ filename: 'source.ts',
+ })
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('evaluates local expressions', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const answer = 42;
+ const foo = () => answer;
+
+ export const Title = styled.h1\`
+ &:before {
+ content: "${"${foo() + ' days'}"}"
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('evaluates functions with nested identifiers', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const objects = { key: { fontSize: 12 } };
+ const foo = (k) => {
+ const obj = objects[k];
+ return obj;
+ };
+
+ export const Title = styled.h1\`
+ ${"${foo('key')}"}
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('evaluates expressions with dependencies', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+ import slugify from '@linaria/babel-preset/__fixtures__/slugify';
+
+ export const Title = styled.h1\`
+ &:before {
+ content: "${"${slugify('test')}"}"
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('evaluates expressions with expressions depending on shared dependency', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ 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 = styled.h1\`
+ &:before {
+ content: "${"${boo('test') + bar('test')}"}"
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('evaluates multiple expressions with shared dependency', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ 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 = styled.h1\`
+ &:before {
+ content: "${"${boo('test')}"}"
+ content: "${"${bar('test')}"}"
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('evaluates interpolations with sequence expression', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Title = styled.h1\`
+ color: ${'${(external, () => "blue")}'};
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('evaluates dependencies with sequence expression', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const color = (external, () => 'blue');
+
+ export const Title = styled.h1\`
+ color: ${'${color}'};
+ \`;
+ `
+ );
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('evaluates component interpolations', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ const { styled } = require('@linaria/react');
+
+ export const Title = styled.h1\`
+ color: red;
+ \`;
+
+ export const Paragraph = styled.p\`
+ ${'${Title}'} {
+ color: blue;
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('throws when interpolation evaluates to undefined', async () => {
+ expect.assertions(1);
+
+ try {
+ await transpile(
+ dedent`
+ const { styled } = require('@linaria/react');
+
+ let fontSize;
+
+ export const Title = styled.h1\`
+ font-size: ${'${fontSize}'};
+ \`;
+ `
+ );
+ } catch (e) {
+ expect(
+ stripAnsi(e.message.replace(dirname, '<<DIRNAME>>'))
+ ).toMatchSnapshot();
+ }
+ });
+
+ it('throws when interpolation evaluates to null', async () => {
+ expect.assertions(1);
+
+ try {
+ await transpile(
+ dedent`
+ const { styled } = require('@linaria/react');
+
+ const color = null;
+
+ export const Title = styled.h1\`
+ color: ${'${color}'};
+ \`;
+ `
+ );
+ } catch (e) {
+ expect(
+ stripAnsi(e.message.replace(dirname, '<<DIRNAME>>'))
+ ).toMatchSnapshot();
+ }
+ });
+
+ it('throws when interpolation evaluates to NaN', async () => {
+ expect.assertions(1);
+
+ try {
+ await transpile(
+ dedent`
+ const { styled } = require('@linaria/react');
+
+ const height = NaN;
+
+ export const Title = styled.h1\`
+ height: ${'${height}'}px;
+ \`;
+ `
+ );
+ } catch (e) {
+ expect(
+ stripAnsi(e.message.replace(dirname, '<<DIRNAME>>'))
+ ).toMatchSnapshot();
+ }
+ });
+
+ it('handles wrapping another styled component', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ const { css } = require('..');
+ const { styled } = require('@linaria/react');
+
+ const Title = styled.h1\`
+ color: red;
+ \`;
+
+ export const BlueTitle = styled(Title)\`
+ font-size: 24px;
+ color: blue;
+ \`;
+
+ export const GreenTitle = styled(BlueTitle)\`
+ color: green;
+ \`;
+ `
+ );
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('handles indirect wrapping another styled component', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ const { styled } = require('@linaria/react');
+
+ const Title = styled.h1\`
+ color: red;
+ \`;
+
+ const hoc = Cmp => Cmp;
+
+ export const CustomTitle = styled(hoc(Title))\`
+ font-size: 24px;
+ color: blue;
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('inlines object styles as CSS string', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const fill = (top = 0, left = 0, right = 0, bottom = 0) => ({
+ position: 'absolute',
+ top,
+ right,
+ bottom,
+ left,
+ });
+
+ export const Title = styled.h1\`
+ ${'${fill(0, 0)}'}
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('inlines array styles as CSS string', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const fill = (top = 0, left = 0, right = 0, bottom = 0) => [
+ { position: 'absolute' },
+ {
+ top,
+ right,
+ bottom,
+ left,
+ }
+ ];
+
+ export const Title = styled.h1\`
+ ${'${fill(0, 0)}'}
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('ignores inline arrow function expressions', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Title = styled.h1\`
+ &:before {
+ content: "${'${props => props.content}'}"
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('simplifies react components', async () => {
+ const div = '<div>{props.children + constant}</div>';
+ const { code, metadata } = await transpile(
+ dedent`
+ import React from 'react';
+ import { styled } from '@linaria/react';
+ import constant from './broken-dependency';
+
+ const FuncComponent = (props) => ${div};
+
+ class ClassComponent extends React.PureComponent {
+ method() {
+ return constant;
+ }
+
+ render() {
+ return ${div};
+ }
+ }
+
+ export const StyledFunc = styled(FuncComponent)\`
+ color: red;
+ \`;
+ export const StyledClass = styled(ClassComponent)\`
+ color: blue;
+ \`;
+ `,
+ (config) => ({
+ ...config,
+ plugins: ['@babel/plugin-syntax-jsx', ...(config.plugins || [])],
+ })
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('ignores inline vanilla function expressions', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export const Title = styled.h1\`
+ &:before {
+ content: "${'${function(props) { return props.content }}'}"
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('ignores external expressions', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const generate = props => props.content;
+
+ export const Title = styled.h1\`
+ &:before {
+ content: "${'${generate}'}"
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('evaluates complex styles with functions and nested selectors', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+ export const bareIconClass = css\`\`;
+
+ const getSizeStyles = (fs) => ({
+ [\`${'&.${bareIconClass}'}\`]: {
+ fontSize: fs * 1.5,
+ },
+ });
+
+ export const SIZES = {
+ XS: css\`${'${getSizeStyles(11)}'}\`,
+ };
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('throws codeframe error when evaluation fails', async () => {
+ expect.assertions(1);
+
+ try {
+ await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const foo = props => { throw new Error('This will fail') };
+
+ export const Title = styled.h1\`
+ font-size: ${'${foo()}'}px;
+ \`;
+ `
+ );
+ } catch (e) {
+ expect(
+ stripAnsi(e.message.replace(dirname, '<<DIRNAME>>'))
+ ).toMatchSnapshot();
+ }
+ });
+
+ it('handles escapes properly', async () => {
+ const { code, metadata } = await transformFileAsync(
+ resolve(__dirname, '../__fixtures__/escape-character.js'),
+ babelrc
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('handles complex component', async () => {
+ const { code, metadata } = await transformFileAsync(
+ resolve(__dirname, '../__fixtures__/complex-component.js'),
+ babelrc
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('generates stable class names', async () => {
+ const { code, metadata } = await transformFileAsync(
+ resolve(__dirname, '../__fixtures__/components-library.js'),
+ babelrc
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('derives display name from filename', async () => {
+ const { code, metadata } = await transformAsync(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export default styled.h1\`
+ font-size: 14px;
+ \`;
+ `,
+ {
+ ...babelrc,
+ filename: join(dirname, 'FancyName.js'),
+ }
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('derives display name from parent folder name', async () => {
+ const { code, metadata } = await transformAsync(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export default styled.h1\`
+ font-size: 14px;
+ \`;
+ `,
+ {
+ ...babelrc,
+ filename: join(dirname, 'FancyName/index.js'),
+ }
+ )!;
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it("throws if couldn't determine a display name", async () => {
+ expect.assertions(1);
+
+ try {
+ await transformAsync(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ export default styled.h1\`
+ font-size: 14px;
+ \`;
+ `,
+ {
+ ...babelrc,
+ filename: join(dirname, '/.js'),
+ }
+ );
+ } catch (e) {
+ expect(
+ stripAnsi(e.message.replace(dirname, '<<DIRNAME>>'))
+ ).toMatchSnapshot();
+ }
+ });
+
+ it('does not strip instanbul coverage sequences', async () => {
+ const { code, metadata } = await transformAsync(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ const a = 42;
+
+ export const Title = styled.h1\`
+ height: ${'${a}'}px;
+ \`;
+ `,
+ {
+ ...babelrc,
+ cwd: '/home/user/project',
+ filename: 'file.js',
+ plugins: [
+ [
+ // eslint-disable-next-line import/no-extraneous-dependencies
+ require('babel-plugin-istanbul').default({
+ ...babel,
+ assertVersion: () => {},
+ }),
+ { cwd: '/home/user/project' },
+ ],
+ ],
+ }
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ // PR #524
+ it('should work with String and Number object', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from '@linaria/core';
+
+ export const style = css\`
+ width: ${'${new String("100%")}'};
+ opacity: ${'${new Number(0.75)}'};
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('should work with generated classnames as selectors', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from "@linaria/core";
+
+ export const text = css\`\`;
+
+ export const square = css\`
+ .${'${text}'} {
+ color: red;
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('should process `css` calls inside components', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import React from 'react'
+ import {css} from '@linaria/core'
+
+ export function Component() {
+ const opacity = 0.2;
+ const className = css\`
+ opacity: ${'${opacity}'};
+ \`;
+
+ return React.createElement("div", { className });
+ }
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('should process `styled` calls inside components', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ 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);
+ }
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('should process `css` calls with complex interpolation inside components', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import React from 'react'
+ import {css} from '@linaria/core'
+ import externalDep from '@linaria/babel-preset/__fixtures__/sample-script';
+ const globalObj = {
+ opacity: 0.5,
+ };
+
+ export function Component() {
+ const classes = {
+ value: 0.2,
+ cell: css\`
+ opacity: 0;
+ \`,
+ };
+
+ const classes2 = classes;
+ const referencedExternalDep = externalDep
+
+ const className = css\`
+ opacity: ${'${globalObj.opacity}'};
+ font-size: ${'${externalDep}'}
+ font-size: ${'${referencedExternalDep}'}
+
+ &:hover .${'${classes2.cell}'} {
+ opacity: ${'${classes.value}'};
+ }
+ \`;
+
+ return React.createElement("div", { className });
+ }
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('should process `styled` calls with complex interpolation inside components', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ 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: css\`
+ opacity: 0;
+ \`,
+ };
+
+ const classes2 = classes;
+
+ const MyComponent = styled\`
+ opacity: ${'${globalObj.opacity}'};
+
+ &:hover .${'${classes2.cell}'} {
+ opacity: ${'${classes.value}'};
+ }
+ ${'${Styled1}'} {
+ font-size: 1;
+ }
+ \`;
+
+ return React.createElement(MyComponent);
+ }
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('should handle shadowed identifier inside components', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import React from 'react'
+ import {css} from '@linaria/core'
+
+ const color = 'red';
+
+ export default function Component() {
+ const color = 'blue'
+ const val = { color };
+ return React.createElement('div', {className: css\`background-color:${'${val.color}'};\`});
+ }
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('it should not throw location error for hoisted identifier', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import React from 'react'
+ import {css} from '@linaria/core'
+
+ const size = () => 5
+ export default function Component() {
+ const color = size()
+ return css\`opacity:${'${color}'};\`
+ }
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ strategyDependentTests(transpile);
+}
diff --git a/@linaria/packages/babel/babel.config.js b/@linaria/packages/babel/babel.config.js
new file mode 100644
index 0000000..c9ad680
--- /dev/null
+++ b/@linaria/packages/babel/babel.config.js
@@ -0,0 +1,3 @@
+const config = require('../../babel.config');
+
+module.exports = config;
diff --git a/@linaria/packages/babel/package.json b/@linaria/packages/babel/package.json
new file mode 100644
index 0000000..a63612c
--- /dev/null
+++ b/@linaria/packages/babel/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "@linaria/babel-preset",
+ "version": "3.0.0-beta.7",
+ "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"
+ },
+ "devDependencies": {
+ "@types/cosmiconfig": "^5.0.3",
+ "@types/dedent": "^0.7.0",
+ "dedent": "^0.7.0",
+ "strip-ansi": "^5.2.0"
+ },
+ "dependencies": {
+ "@babel/generator": ">=7",
+ "@babel/plugin-syntax-dynamic-import": ">=7",
+ "@babel/template": ">=7",
+ "@linaria/core": "^3.0.0-beta.4",
+ "@linaria/logger": "^3.0.0-beta.3",
+ "cosmiconfig": "^5.1.0",
+ "source-map": "^0.7.3",
+ "stylis": "^3.5.4"
+ },
+ "peerDependencies": {
+ "@babel/core": ">=7"
+ }
+}
diff --git a/@linaria/packages/babel/src/babel.ts b/@linaria/packages/babel/src/babel.ts
new file mode 100644
index 0000000..d99a54b
--- /dev/null
+++ b/@linaria/packages/babel/src/babel.ts
@@ -0,0 +1,3 @@
+import type core from '@babel/core';
+
+export type Core = typeof core;
diff --git a/@linaria/packages/babel/src/dynamic-import-noop.ts b/@linaria/packages/babel/src/dynamic-import-noop.ts
new file mode 100644
index 0000000..879fdc4
--- /dev/null
+++ b/@linaria/packages/babel/src/dynamic-import-noop.ts
@@ -0,0 +1,26 @@
+import type { Import } from '@babel/types';
+import type { NodePath } from '@babel/traverse';
+import syntax from '@babel/plugin-syntax-dynamic-import';
+import type { Visitor } from '@babel/traverse';
+import { Core } from './babel';
+
+export default function dynamic({ types: t }: Core): {
+ inherits: any;
+ visitor: Visitor;
+} {
+ return {
+ inherits: syntax,
+ visitor: {
+ Import(path: NodePath<Import>) {
+ const noop = t.arrowFunctionExpression([], t.identifier('undefined'));
+
+ path.parentPath.replaceWith(
+ t.objectExpression([
+ t.objectProperty(t.identifier('then'), noop),
+ t.objectProperty(t.identifier('catch'), noop),
+ ])
+ );
+ },
+ },
+ };
+}
diff --git a/@linaria/packages/babel/src/eval-cache.ts b/@linaria/packages/babel/src/eval-cache.ts
new file mode 100644
index 0000000..96149a6
--- /dev/null
+++ b/@linaria/packages/babel/src/eval-cache.ts
@@ -0,0 +1,87 @@
+import { createHash } from 'crypto';
+import { debug } from '@linaria/logger';
+
+const fileHashes = new Map<string, string>();
+const evalCache = new Map<string, any>();
+const fileKeys = new Map<string, string[]>();
+
+const hash = (text: string) => createHash('sha1').update(text).digest('base64');
+
+let lastText: string = '';
+let lastHash: string = hash(lastText);
+
+const memoizedHash: typeof hash = (text) => {
+ if (lastText !== text) {
+ lastHash = hash(text);
+ lastText = text;
+ }
+
+ return lastHash;
+};
+
+const toKey = (filename: string, exports: string[]) =>
+ exports.length > 0 ? `${filename}:${exports.join(',')}` : filename;
+
+export const clear = () => {
+ fileHashes.clear();
+ evalCache.clear();
+ fileKeys.clear();
+};
+
+export const clearForFile = (filename: string) => {
+ const keys = fileKeys.get(filename) ?? [];
+ if (keys.length === 0) {
+ return;
+ }
+
+ debug('eval-cache:clear-for-file', filename);
+
+ for (const key of keys) {
+ fileHashes.delete(key);
+ evalCache.delete(key);
+ }
+
+ fileKeys.set(filename, []);
+};
+
+export const has = (
+ [filename, ...exports]: string[],
+ text: string
+): boolean => {
+ const key = toKey(filename, exports);
+ const textHash = memoizedHash(text);
+ debug('eval-cache:has', `${key} ${textHash}`);
+
+ return fileHashes.get(key) === textHash;
+};
+
+export const get = ([filename, ...exports]: string[], text: string): any => {
+ const key = toKey(filename, exports);
+ const textHash = memoizedHash(text);
+ debug('eval-cache:get', `${key} ${textHash}`);
+
+ if (fileHashes.get(key) !== textHash) {
+ return undefined;
+ }
+
+ return evalCache.get(key);
+};
+
+export const set = (
+ [filename, ...exports]: string[],
+ text: string,
+ value: any
+): void => {
+ const key = toKey(filename, exports);
+ const textHash = memoizedHash(text);
+ debug('eval-cache:set', `${key} ${textHash}`);
+
+ fileHashes.set(key, textHash);
+ evalCache.set(key, value);
+
+ if (!fileKeys.has(filename)) {
+ fileKeys.set(filename, []);
+ }
+
+ fileKeys.get(filename)!.push(key);
+};
diff --git a/@linaria/packages/babel/src/evaluators/buildOptions.ts b/@linaria/packages/babel/src/evaluators/buildOptions.ts
new file mode 100644
index 0000000..a938d4b
--- /dev/null
+++ b/@linaria/packages/babel/src/evaluators/buildOptions.ts
@@ -0,0 +1,104 @@
+/**
+ * This file handles preparing babel config for Linaria preevaluation.
+ */
+
+import type { PluginItem, TransformOptions } from '@babel/core';
+import type { StrictOptions } from '../types';
+
+type DefaultOptions = Partial<TransformOptions> & {
+ plugins: PluginItem[];
+ presets: PluginItem[];
+ caller: { evaluate: boolean };
+};
+
+export default function buildOptions(
+ filename: string,
+ options?: StrictOptions
+): TransformOptions {
+ const plugins: Array<string | object> = [
+ // Include these plugins to avoid extra config when using { module: false } for webpack
+ '@babel/plugin-transform-modules-commonjs',
+ '@babel/plugin-proposal-export-namespace-from',
+ ];
+
+ const defaults: DefaultOptions = {
+ caller: { name: 'linaria', evaluate: true },
+ filename: filename,
+ presets: [
+ [
+ require.resolve('../index'),
+ {
+ ...(options || {}),
+ },
+ ],
+ ],
+ plugins: [
+ ...plugins.map((name) => require.resolve(name as string)),
+ // We don't support dynamic imports when evaluating, but don't wanna syntax error
+ // This will replace dynamic imports with an object that does nothing
+ require.resolve('../dynamic-import-noop'),
+ ],
+ };
+
+ const babelOptions =
+ // Shallow copy the babel options because we mutate it later
+ options?.babelOptions ? { ...options.babelOptions } : {};
+
+ // If we programmatically pass babel options while there is a .babelrc, babel might throw
+ // We need to filter out duplicate presets and plugins so that this doesn't happen
+ // This workaround isn't full proof, but it's still better than nothing
+ const keys: Array<keyof TransformOptions & ('presets' | 'plugins')> = [
+ 'presets',
+ 'plugins',
+ ];
+ keys.forEach((field) => {
+ babelOptions[field] = babelOptions[field]
+ ? babelOptions[field]!.filter((item: PluginItem) => {
+ // If item is an array it's a preset/plugin with options ([preset, options])
+ // Get the first item to get the preset.plugin name
+ // Otherwise it's a plugin name (can be a function too)
+ const name = Array.isArray(item) ? item[0] : item;
+
+ if (
+ // In our case, a preset might also be referring to linaria/babel
+ // We require the file from internal path which is not the same one that we export
+ // This case won't get caught and the preset won't filtered, even if they are same
+ // So we add an extra check for top level linaria/babel
+ name === 'linaria/babel' ||
+ name === '@linaria' ||
+ name === '@linaria/babel-preset' ||
+ name === require.resolve('../index') ||
+ // Also add a check for the plugin names we include for bundler support
+ plugins.includes(name)
+ ) {
+ return false;
+ }
+
+ // Loop through the default presets/plugins to see if it already exists
+ return !defaults[field].some((it) =>
+ // The default presets/plugins can also have nested arrays,
+ Array.isArray(it) ? it[0] === name : it === name
+ );
+ })
+ : [];
+ });
+
+ return {
+ // Passed options shouldn't be able to override the options we pass
+ // Linaria's plugins rely on these (such as filename to generate consistent hash)
+ ...babelOptions,
+ ...defaults,
+ presets: [
+ // Preset order is last to first, so add the extra presets to start
+ // This makes sure that our preset is always run first
+ ...babelOptions.presets!,
+ ...defaults.presets,
+ ],
+ plugins: [
+ ...defaults.plugins,
+ // Plugin order is first to last, so add the extra presets to end
+ // This makes sure that the plugins we specify always run first
+ ...babelOptions.plugins!,
+ ],
+ };
+}
diff --git a/@linaria/packages/babel/src/evaluators/index.ts b/@linaria/packages/babel/src/evaluators/index.ts
new file mode 100644
index 0000000..1d24750
--- /dev/null
+++ b/@linaria/packages/babel/src/evaluators/index.ts
@@ -0,0 +1,22 @@
+/**
+ * This file is an entry point for module evaluation for getting lazy dependencies.
+ */
+
+import Module from '../module';
+import type { StrictOptions } from '../types';
+
+export default function evaluate(
+ code: string,
+ filename: string,
+ options: StrictOptions
+) {
+ const m = new Module(filename, options);
+
+ m.dependencies = [];
+ m.evaluate(code, ['__linariaPreval']);
+
+ return {
+ value: m.exports,
+ dependencies: m.dependencies,
+ };
+}
diff --git a/@linaria/packages/babel/src/evaluators/templateProcessor.ts b/@linaria/packages/babel/src/evaluators/templateProcessor.ts
new file mode 100644
index 0000000..0c910d4
--- /dev/null
+++ b/@linaria/packages/babel/src/evaluators/templateProcessor.ts
@@ -0,0 +1,313 @@
+/**
+ * This file handles transforming template literals to class names or styled components and generates CSS content.
+ * It uses CSS code from template literals and evaluated values of lazy dependencies stored in ValueCache.
+ */
+
+import type { Expression } from '@babel/types';
+import generator from '@babel/generator';
+
+import type { StyledMeta } from '@linaria/core';
+import { debug } from '@linaria/logger';
+import { units } from '../units';
+import type {
+ State,
+ StrictOptions,
+ TemplateExpression,
+ ValueCache,
+} from '../types';
+
+import isSerializable from '../utils/isSerializable';
+import throwIfInvalid from '../utils/throwIfInvalid';
+import stripLines from '../utils/stripLines';
+import toCSS from '../utils/toCSS';
+import getLinariaComment from '../utils/getLinariaComment';
+import { Core } from '../babel';
+
+// Match any valid CSS units followed by a separator such as ;, newline etc.
+const unitRegex = new RegExp(`^(${units.join('|')})(;|,|\n| |\\))`);
+
+type Interpolation = {
+ id: string;
+ node: Expression;
+ source: string;
+ unit: string;
+};
+
+function hasMeta(value: any): value is StyledMeta {
+ return value && typeof value === 'object' && (value as any).__linaria;
+}
+
+const processedPaths = new WeakSet();
+
+export default function getTemplateProcessor(
+ { types: t }: Core,
+ options: StrictOptions
+) {
+ return function process(
+ { styled, path }: TemplateExpression,
+ state: State,
+ valueCache: ValueCache
+ ) {
+ if (processedPaths.has(path)) {
+ // Do not process an expression
+ // if it is referenced in one template more than once
+ return;
+ }
+
+ processedPaths.add(path);
+
+ const { quasi } = path.node;
+
+ const interpolations: Interpolation[] = [];
+
+ // Check if the variable is referenced anywhere for basic DCE
+ // Only works when it's assigned to a variable
+ let isReferenced = true;
+
+ const [, slug, displayName, className] = getLinariaComment(path);
+
+ const parent = path.findParent(
+ (p) =>
+ t.isObjectProperty(p) ||
+ t.isJSXOpeningElement(p) ||
+ t.isVariableDeclarator(p)
+ );
+
+ if (parent) {
+ const parentNode = parent.node;
+ if (t.isVariableDeclarator(parentNode) && t.isIdentifier(parentNode.id)) {
+ const { referencePaths } = path.scope.getBinding(
+ parentNode.id.name
+ ) || { referencePaths: [] };
+
+ isReferenced = referencePaths.length !== 0;
+ }
+ }
+
+ // Serialize the tagged template literal to a string
+ let cssText = '';
+
+ const expressions = path.get('quasi').get('expressions');
+
+ quasi.quasis.forEach((el, i, self) => {
+ let appended = false;
+
+ if (i !== 0 && el.value.cooked) {
+ // Check if previous expression was a CSS variable that we replaced
+ // If it has a unit after it, we need to move the unit into the interpolation
+ // e.g. `var(--size)px` should actually be `var(--size)`
+ // So we check if the current text starts with a unit, and add the unit to the previous interpolation
+ // Another approach would be `calc(var(--size) * 1px), but some browsers don't support all units
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=956573
+ const matches = el.value.cooked.match(unitRegex);
+
+ if (matches) {
+ const last = interpolations[interpolations.length - 1];
+ const [, unit] = matches;
+
+ if (last && cssText.endsWith(`var(--${last.id})`)) {
+ last.unit = unit;
+ cssText += el.value.cooked.replace(unitRegex, '$2');
+ appended = true;
+ }
+ }
+ }
+
+ if (!appended) {
+ cssText += el.value.cooked;
+ }
+
+ const ex = expressions[i];
+
+ if (ex && !ex.isExpression()) {
+ throw ex.buildCodeFrameError(
+ `The expression '${generator(ex.node).code}' is not supported.`
+ );
+ }
+
+ if (ex) {
+ const { end } = ex.node.loc!;
+ const result = ex.evaluate();
+ const beforeLength = cssText.length;
+
+ // The location will be end of the current string to start of next string
+ const next = self[i + 1];
+ const loc = {
+ // +1 because the expressions location always shows 1 column before
+ start: { line: el.loc!.end.line, column: el.loc!.end.column + 1 },
+ end: next
+ ? { line: next.loc!.start.line, column: next.loc!.start.column }
+ : { line: end.line, column: end.column + 1 },
+ };
+
+ if (result.confident) {
+ throwIfInvalid(result.value, ex);
+
+ if (isSerializable(result.value)) {
+ // If it's a plain object or an array, convert it to a CSS string
+ cssText += stripLines(loc, toCSS(result.value));
+ } else {
+ cssText += stripLines(loc, result.value);
+ }
+
+ state.replacements.push({
+ original: loc,
+ length: cssText.length - beforeLength,
+ });
+ } else {
+ // Try to preval the value
+ if (
+ options.evaluate &&
+ !(t.isFunctionExpression(ex) || t.isArrowFunctionExpression(ex))
+ ) {
+ const value = valueCache.get(ex.node);
+ throwIfInvalid(value, ex);
+
+ // Skip the blank string instead of throw ing an error
+ if (value === '') {
+ return;
+ }
+
+ if (value && typeof value !== 'function') {
+ // Only insert text for non functions
+ // We don't touch functions because they'll be interpolated at runtime
+
+ if (hasMeta(value)) {
+ // If it's an React component wrapped in styled, get the class name
+ // Useful for interpolating components
+ cssText += `.${value.__linaria.className}`;
+ } else if (isSerializable(value)) {
+ cssText += stripLines(loc, toCSS(value));
+ } else {
+ // For anything else, assume it'll be stringified
+ cssText += stripLines(loc, value);
+ }
+
+ state.replacements.push({
+ original: loc,
+ length: cssText.length - beforeLength,
+ });
+
+ return;
+ }
+ }
+
+ if (styled) {
+ const id = `${slug}-${i}`;
+
+ interpolations.push({
+ id,
+ node: ex.node,
+ source: ex.getSource() || generator(ex.node).code,
+ unit: '',
+ });
+
+ cssText += `var(--${id})`;
+ } else {
+ // CSS custom properties can't be used outside components
+ throw ex.buildCodeFrameError(
+ `The CSS cannot contain JavaScript expressions when using the 'css' tag. To evaluate the expressions at build time, pass 'evaluate: true' to the babel plugin.`
+ );
+ }
+ }
+ }
+ });
+
+ let selector = `.${className}`;
+
+ if (styled) {
+ // If `styled` wraps another component and not a primitive,
+ // get its class name to create a more specific selector
+ // it'll ensure that styles are overridden properly
+ if (options.evaluate && t.isIdentifier(styled.component.node)) {
+ let value = valueCache.get(styled.component.node.name);
+ while (hasMeta(value)) {
+ selector += `.${value.__linaria.className}`;
+ value = value.__linaria.extends;
+ }
+ }
+
+ const props = [];
+
+ props.push(
+ t.objectProperty(t.identifier('name'), t.stringLiteral(displayName!))
+ );
+
+ props.push(
+ t.objectProperty(t.identifier('class'), t.stringLiteral(className!))
+ );
+
+ // If we found any interpolations, also pass them so they can be applied
+ if (interpolations.length) {
+ // De-duplicate interpolations based on the source and unit
+ // If two interpolations have the same source code and same unit,
+ // we don't need to use 2 custom properties for them, we can use a single one
+ const result: { [key: string]: Interpolation } = {};
+
+ interpolations.forEach((it) => {
+ const key = it.source + it.unit;
+
+ if (key in result) {
+ cssText = cssText.replace(
+ `var(--${it.id})`,
+ `var(--${result[key].id})`
+ );
+ } else {
+ result[key] = it;
+ }
+ });
+
+ props.push(
+ t.objectProperty(
+ t.identifier('vars'),
+ t.objectExpression(
+ Object.keys(result).map((key) => {
+ const { id, node, unit } = result[key];
+ const items = [node];
+
+ if (unit) {
+ items.push(t.stringLiteral(unit));
+ }
+
+ return t.objectProperty(
+ t.stringLiteral(id),
+ t.arrayExpression(items)
+ );
+ })
+ )
+ )
+ );
+ }
+
+ path.replaceWith(
+ t.callExpression(
+ t.callExpression(
+ t.identifier(state.file.metadata.localName || 'styled'),
+ [styled.component.node]
+ ),
+ [t.objectExpression(props)]
+ )
+ );
+
+ path.addComment('leading', '#__PURE__');
+ } else {
+ path.replaceWith(t.stringLiteral(className!));
+ }
+
+ if (!isReferenced && !cssText.includes(':global')) {
+ return;
+ }
+
+ debug(
+ 'evaluator:template-processor:extracted-rule',
+ `\n${selector} {${cssText}\n}`
+ );
+
+ state.rules[selector] = {
+ cssText,
+ className: className!,
+ displayName: displayName!,
+ start: path.parent?.loc?.start ?? null,
+ };
+ };
+}
diff --git a/@linaria/packages/babel/src/evaluators/visitors/JSXElement.ts b/@linaria/packages/babel/src/evaluators/visitors/JSXElement.ts
new file mode 100644
index 0000000..89a0ccd
--- /dev/null
+++ b/@linaria/packages/babel/src/evaluators/visitors/JSXElement.ts
@@ -0,0 +1,56 @@
+import { types as t } from '@babel/core';
+import type { NodePath } from '@babel/traverse';
+import type { Function, JSXElement as JSXElementNode } from '@babel/types';
+
+function getFunctionName(path: NodePath<Function>): string | null {
+ if (path.isClassMethod() && t.isIdentifier(path.node.key)) {
+ return path.node.key.name;
+ }
+
+ return null;
+}
+
+export default function JSXElement(path: NodePath<JSXElementNode>) {
+ // JSX can be safely replaced on an empty fragment because it is unnecessary for styles
+ const emptyFragment = t.jsxFragment(
+ t.jsxOpeningFragment(),
+ t.jsxClosingFragment(),
+ []
+ );
+
+ // We can do even more
+ // If that JSX is a result of a function, we can replace the function body.
+ const scopePath = path.scope.path;
+ if (scopePath.isFunction()) {
+ const emptyBody = t.blockStatement([t.returnStatement(emptyFragment)]);
+
+ // Is it not just a function, but a method `render`?
+ if (getFunctionName(scopePath) === 'render') {
+ const decl = scopePath.findParent((p) => p.isClassDeclaration());
+
+ // Replace the whole component
+ if (decl?.isClassDeclaration()) {
+ decl.replaceWith(t.functionDeclaration(decl.node.id, [], emptyBody));
+
+ return;
+ }
+ }
+
+ const body = scopePath.get('body');
+ if (Array.isArray(body)) {
+ throw new Error(
+ `A body of a function is expected to be a single element but an array was returned. It's possible if JS syntax has been changed since that code was written.`
+ );
+ }
+
+ const node: typeof scopePath.node = {
+ ...scopePath.node,
+ body: emptyBody,
+ params: [],
+ };
+
+ scopePath.replaceWith(node);
+ } else {
+ path.replaceWith(emptyFragment);
+ }
+}
diff --git a/@linaria/packages/babel/src/evaluators/visitors/ProcessCSS.ts b/@linaria/packages/babel/src/evaluators/visitors/ProcessCSS.ts
new file mode 100644
index 0000000..6f737a1
--- /dev/null
+++ b/@linaria/packages/babel/src/evaluators/visitors/ProcessCSS.ts
@@ -0,0 +1,20 @@
+/**
+ * This visitor replaces css tag with the generated className
+ *
+ */
+
+import { types as t } from '@babel/core';
+import type { NodePath } from '@babel/traverse';
+import type { TaggedTemplateExpression } from '@babel/types';
+import getLinariaComment from '../../utils/getLinariaComment';
+
+export default function ProcessCSS(path: NodePath<TaggedTemplateExpression>) {
+ if (t.isIdentifier(path.node.tag) && path.node.tag.name === 'css') {
+ const [, , , className] = getLinariaComment(path);
+ if (!className) {
+ return;
+ }
+
+ path.replaceWith(t.stringLiteral(className));
+ }
+}
diff --git a/@linaria/packages/babel/src/evaluators/visitors/ProcessStyled.ts b/@linaria/packages/babel/src/evaluators/visitors/ProcessStyled.ts
new file mode 100644
index 0000000..a9e38c5
--- /dev/null
+++ b/@linaria/packages/babel/src/evaluators/visitors/ProcessStyled.ts
@@ -0,0 +1,46 @@
+/**
+ * This visitor replaces styled components with metadata about them.
+ * CallExpression should be used to match styled components.
+ * Works out of the box for styled that wraps other component,
+ * styled.tagName are transformed to call expressions using @babel/plugin-transform-template-literals
+ * @babel/plugin-transform-template-literals is loaded as a prest, to force proper ordering. It has to run just after linaria.
+ * It is used explicitly in extractor, and loaded as a part of `prest-env` in shaker
+ */
+
+import { types as t } from '@babel/core';
+import type { NodePath } from '@babel/traverse';
+import type { CallExpression } from '@babel/types';
+import { expression } from '@babel/template';
+import getLinariaComment from '../../utils/getLinariaComment';
+
+const linariaComponentTpl = expression(
+ `{
+ displayName: %%displayName%%,
+ __linaria: {
+ className: %%className%%,
+ extends: %%extends%%
+ }
+ }`
+);
+
+export default function ProcessStyled(path: NodePath<CallExpression>) {
+ const [type, , displayName, className] = getLinariaComment(path);
+ if (!className) {
+ return;
+ }
+
+ if (type === 'css') {
+ path.replaceWith(t.stringLiteral(className));
+ return;
+ }
+
+ path.replaceWith(
+ linariaComponentTpl({
+ className: t.stringLiteral(className),
+ displayName: displayName ? t.stringLiteral(displayName) : null,
+ extends: t.isCallExpression(path.node.callee)
+ ? path.node.callee.arguments[0]
+ : t.nullLiteral(),
+ })
+ );
+}
diff --git a/@linaria/packages/babel/src/extract.ts b/@linaria/packages/babel/src/extract.ts
new file mode 100644
index 0000000..fb2df83
--- /dev/null
+++ b/@linaria/packages/babel/src/extract.ts
@@ -0,0 +1,222 @@
+/* eslint-disable no-param-reassign */
+
+/**
+ * This is an entry point for styles extraction.
+ * On enter, It:
+ * - traverse the code using visitors (TaggedTemplateExpression, ImportDeclaration)
+ * - schedule evaluation of lazy dependencies (those who are not simple expressions //TODO does they have it's name?)
+ * - let templateProcessor to save evaluated values in babel state as `replacements`.
+ * On exit, It:
+ * - store result of extraction in babel's file metadata
+ */
+
+import type { Node, Program, Expression } from '@babel/types';
+import type { NodePath, Scope, Visitor } from '@babel/traverse';
+import { expression, statement } from '@babel/template';
+import generator from '@babel/generator';
+import { debug, error } from '@linaria/logger';
+import evaluate from './evaluators';
+import getTemplateProcessor from './evaluators/templateProcessor';
+import Module from './module';
+import type {
+ State,
+ StrictOptions,
+ LazyValue,
+ ExpressionValue,
+ ValueCache,
+} from './types';
+import { ValueType } from './types';
+import CollectDependencies from './visitors/CollectDependencies';
+import DetectStyledImportName from './visitors/DetectStyledImportName';
+import GenerateClassNames from './visitors/GenerateClassNames';
+import type { Core } from './babel';
+
+function isLazyValue(v: ExpressionValue): v is LazyValue {
+ return v.kind === ValueType.LAZY;
+}
+
+function isNodePath<T extends Node>(obj: NodePath<T> | T): obj is NodePath<T> {
+ return 'node' in obj && obj?.node !== undefined;
+}
+
+function findFreeName(scope: Scope, name: string): string {
+ // By default `name` is used as a name of the function …
+ let nextName = name;
+ let idx = 0;
+ while (scope.hasBinding(nextName, false)) {
+ // … but if there is an already defined variable with this name …
+ // … we are trying to use a name like wrap_N
+ idx += 1;
+ nextName = `wrap_${idx}`;
+ }
+
+ return nextName;
+}
+
+function unwrapNode<T extends Node>(
+ item: NodePath<T> | T | string
+): T | string {
+ if (typeof item === 'string') {
+ return item;
+ }
+
+ return isNodePath(item) ? item.node : item;
+}
+
+// All exported values will be wrapped with this function
+const expressionWrapperTpl = statement(`
+ const %%wrapName%% = (fn) => {
+ try {
+ return fn();
+ } catch (e) {
+ return e;
+ }
+ };
+`);
+
+const expressionTpl = expression(`%%wrapName%%(() => %%expression%%)`);
+const exportsLinariaPrevalTpl = statement(
+ `exports.__linariaPreval = %%expressions%%`
+);
+
+function addLinariaPreval(
+ { types: t }: Core,
+ path: NodePath<Program>,
+ lazyDeps: Array<Expression | string>
+): Program {
+ // Constant __linariaPreval with all dependencies
+ const wrapName = findFreeName(path.scope, '_wrap');
+ const statements = [
+ expressionWrapperTpl({ wrapName }),
+ exportsLinariaPrevalTpl({
+ expressions: t.arrayExpression(
+ lazyDeps.map((expression) => expressionTpl({ expression, wrapName }))
+ ),
+ }),
+ ];
+
+ const programNode = path.node;
+ return t.program(
+ [...programNode.body, ...statements],
+ programNode.directives,
+ programNode.sourceType,
+ programNode.interpreter
+ );
+}
+
+export default function extract(
+ babel: Core,
+ options: StrictOptions
+): { visitor: Visitor<State> } {
+ const process = getTemplateProcessor(babel, options);
+
+ return {
+ visitor: {
+ Program: {
+ enter(path: NodePath<Program>, state: State) {
+ // Collect all the style rules from the styles we encounter
+ state.queue = [];
+ state.rules = {};
+ state.index = -1;
+ state.dependencies = [];
+ state.replacements = [];
+ debug('extraction:start', state.file.opts.filename);
+
+ // Invalidate cache for module evaluation to get fresh modules
+ Module.invalidate();
+
+ // We need our transforms to run before anything else
+ // So we traverse here instead of a in a visitor
+ path.traverse({
+ ImportDeclaration: (p) => DetectStyledImportName(babel, p, state),
+ TaggedTemplateExpression: (p) => {
+ GenerateClassNames(babel, p, state, options);
+ CollectDependencies(babel, p, state, options);
+ },
+ });
+
+ const lazyDeps = state.queue.reduce(
+ (acc, { expressionValues: values }) => {
+ acc.push(...values.filter(isLazyValue));
+ return acc;
+ },
+ [] as LazyValue[]
+ );
+
+ const expressionsToEvaluate = lazyDeps.map((v) => unwrapNode(v.ex));
+ const originalLazyExpressions = lazyDeps.map((v) =>
+ unwrapNode(v.originalEx)
+ );
+
+ debug('lazy-deps:count', lazyDeps.length);
+
+ let lazyValues: any[] = [];
+
+ if (expressionsToEvaluate.length > 0) {
+ debug(
+ 'lazy-deps:original-expressions-list',
+ originalLazyExpressions.map((node) =>
+ typeof node !== 'string' ? generator(node).code : node
+ )
+ );
+ debug(
+ 'lazy-deps:expressions-to-eval-list',
+ expressionsToEvaluate.map((node) =>
+ typeof node !== 'string' ? generator(node).code : node
+ )
+ );
+
+ const program = addLinariaPreval(
+ babel,
+ path,
+ expressionsToEvaluate
+ );
+ const { code } = generator(program);
+ debug('lazy-deps:evaluate', '');
+ try {
+ const evaluation = evaluate(
+ code,
+ state.file.opts.filename,
+ options
+ );
+ debug('lazy-deps:sub-files', evaluation.dependencies);
+
+ state.dependencies.push(...evaluation.dependencies);
+ lazyValues = evaluation.value.__linariaPreval || [];
+ debug('lazy-deps:values', evaluation.value.__linariaPreval);
+ } catch (e) {
+ error('lazy-deps:evaluate', code);
+ throw new Error(
+ 'An unexpected runtime error occurred during dependencies evaluation: \n' +
+ e.stack +
+ '\n\nIt may happen when your code or third party module is invalid or uses identifiers not available in Node environment, eg. window. \n' +
+ 'Note that line numbers in above stack trace will most likely not match, because Linaria needed to transform your code a bit.\n'
+ );
+ }
+ }
+
+ const valueCache: ValueCache = new Map();
+ originalLazyExpressions.forEach((key, idx) =>
+ valueCache.set(key, lazyValues[idx])
+ );
+ state.queue.forEach((item) => process(item, state, valueCache));
+ },
+ exit(_: any, state: State) {
+ if (Object.keys(state.rules).length) {
+ // Store the result as the file metadata under linaria key
+ state.file.metadata.linaria = {
+ rules: state.rules,
+ replacements: state.replacements,
+ dependencies: state.dependencies,
+ };
+ }
+
+ // Invalidate cache for module evaluation when we're done
+ Module.invalidate();
+
+ debug('extraction:end', state.file.opts.filename);
+ },
+ },
+ },
+ };
+}
diff --git a/@linaria/packages/babel/src/index.ts b/@linaria/packages/babel/src/index.ts
new file mode 100644
index 0000000..048fe58
--- /dev/null
+++ b/@linaria/packages/babel/src/index.ts
@@ -0,0 +1,45 @@
+/**
+ * File defines babel prest for Linaria.
+ * It uses ./extract function that is an entry point for styles extraction.
+ * It also bypass babel options defined in Linaria config file with it's defaults (see ./utils/loadOptions).
+ */
+import type { ConfigAPI, TransformCaller } from '@babel/core';
+
+import { debug } from '@linaria/logger';
+import type { PluginOptions } from './utils/loadOptions';
+import loadOptions from './utils/loadOptions';
+
+export * as EvalCache from './eval-cache';
+export { default as buildOptions } from './evaluators/buildOptions';
+export { default as JSXElement } from './evaluators/visitors/JSXElement';
+export { default as ProcessCSS } from './evaluators/visitors/ProcessCSS';
+export { default as ProcessStyled } from './evaluators/visitors/ProcessStyled';
+export { default as Module } from './module';
+export {
+ default as transform,
+ extractCssFromAst,
+ shouldTransformCode,
+} from './transform';
+export * from './types';
+export type { PluginOptions } from './utils/loadOptions';
+export { default as isNode } from './utils/isNode';
+export { default as getVisitorKeys } from './utils/getVisitorKeys';
+export { default as peek } from './utils/peek';
+export { default as slugify } from './utils/slugify';
+export { default as CollectDependencies } from './visitors/CollectDependencies';
+export { default as DetectStyledImportName } from './visitors/DetectStyledImportName';
+export { default as GenerateClassNames } from './visitors/GenerateClassNames';
+
+function isEnabled(caller?: TransformCaller & { evaluate?: true }) {
+ return caller?.name !== 'linaria' || !caller.evaluate;
+}
+
+export default function linaria(babel: ConfigAPI, options: PluginOptions) {
+ if (!babel.caller(isEnabled)) {
+ return {};
+ }
+ debug('options', JSON.stringify(options));
+ return {
+ plugins: [[require('./extract'), loadOptions(options)]],
+ };
+}
diff --git a/@linaria/packages/babel/src/module.ts b/@linaria/packages/babel/src/module.ts
new file mode 100644
index 0000000..7893812
--- /dev/null
+++ b/@linaria/packages/babel/src/module.ts
@@ -0,0 +1,373 @@
+/**
+ * This is a custom implementation for the module system for evaluating code,
+ * used for resolving values for dependencies interpolated in `css` or `styled`.
+ *
+ * This serves 2 purposes:
+ * - Avoid leakage from evaluated code to module cache in current context, e.g. `babel-register`
+ * - Allow us to invalidate the module cache without affecting other stuff, necessary for rebuilds
+ *
+ * We also use it to transpile the code with Babel by default.
+ * We also store source maps for it to provide correct error stacktraces.
+ *
+ */
+
+import NativeModule from 'module';
+import vm from 'vm';
+import fs from 'fs';
+import path from 'path';
+import type { BabelFileResult } from '@babel/core';
+import { debug } from '@linaria/logger';
+import * as EvalCache from './eval-cache';
+import * as process from './process';
+import type { Evaluator, StrictOptions } from './types';
+
+// Supported node builtins based on the modules polyfilled by webpack
+// `true` means module is polyfilled, `false` means module is empty
+const builtins = {
+ assert: true,
+ buffer: true,
+ child_process: false,
+ cluster: false,
+ console: true,
+ constants: true,
+ crypto: true,
+ dgram: false,
+ dns: false,
+ domain: true,
+ events: true,
+ fs: false,
+ http: true,
+ https: true,
+ module: false,
+ net: false,
+ os: true,
+ path: true,
+ punycode: true,
+ process: true,
+ querystring: true,
+ readline: false,
+ repl: false,
+ stream: true,
+ string_decoder: true,
+ sys: true,
+ timers: true,
+ tls: false,
+ tty: true,
+ url: true,
+ util: true,
+ vm: true,
+ zlib: true,
+};
+
+// Separate cache for evaluated modules
+let cache: { [id: string]: Module } = {};
+
+const NOOP = () => {};
+
+const createCustomDebug =
+ (depth: number) =>
+ (..._args: Parameters<typeof debug>) => {
+ const [namespaces, arg1, ...args] = _args;
+ const modulePrefix = depth === 0 ? 'module' : `sub-module-${depth}`;
+ debug(`${modulePrefix}:${namespaces}`, arg1, ...args);
+ };
+
+const cookModuleId = (rawId: string) => {
+ // It's a dirty hack for avoiding conflicts with babel-preset-react-app
+ // https://github.com/callstack/linaria/issues/745
+ // FIXME @Anber: I'll try to figure out a better solution. Probably, using Terser as a shaker's core can solve problems with interfered plugins.
+ return rawId.replace(
+ '/@babel/runtime/helpers/esm/',
+ '/@babel/runtime/helpers/'
+ );
+};
+
+class Module {
+ static invalidate: () => void;
+ static invalidateEvalCache: () => void;
+ static _resolveFilename: (
+ id: string,
+ options: { id: string; filename: string; paths: string[] }
+ ) => string;
+ static _nodeModulePaths: (filename: string) => string[];
+
+ id: string;
+ filename: string;
+ options: StrictOptions;
+ imports: Map<string, string[]> | null;
+ paths: string[];
+ exports: any;
+ extensions: string[];
+ dependencies: string[] | null;
+ transform: ((text: string) => BabelFileResult | null) | null;
+ debug: typeof debug;
+ debuggerDepth: number;
+
+ constructor(
+ filename: string,
+ options: StrictOptions,
+ debuggerDepth: number = 0
+ ) {
+ this.id = filename;
+ this.filename = filename;
+ this.options = options;
+ this.imports = null;
+ this.paths = [];
+ this.dependencies = null;
+ this.transform = null;
+ this.debug = createCustomDebug(debuggerDepth);
+ this.debuggerDepth = debuggerDepth;
+
+ Object.defineProperties(this, {
+ id: {
+ value: filename,
+ writable: false,
+ },
+ filename: {
+ value: filename,
+ writable: false,
+ },
+ paths: {
+ value: Object.freeze(
+ (
+ NativeModule as unknown as {
+ _nodeModulePaths(filename: string): string[];
+ }
+ )._nodeModulePaths(path.dirname(filename))
+ ),
+ writable: false,
+ },
+ });
+
+ this.exports = {};
+
+ // We support following extensions by default
+ this.extensions = ['.json', '.js', '.jsx', '.ts', '.tsx'];
+ this.debug('prepare', filename);
+ }
+
+ resolve = (rawId: string) => {
+ const id = cookModuleId(rawId);
+ const extensions = (
+ NativeModule as unknown as {
+ _extensions: { [key: string]: Function };
+ }
+ )._extensions;
+ const added: string[] = [];
+
+ try {
+ // Check for supported extensions
+ this.extensions.forEach((ext) => {
+ if (ext in extensions) {
+ return;
+ }
+
+ // When an extension is not supported, add it
+ // And keep track of it to clean it up after resolving
+ // Use noop for the transform function since we handle it
+ extensions[ext] = NOOP;
+ added.push(ext);
+ });
+
+ return Module._resolveFilename(id, this);
+ } finally {
+ // Cleanup the extensions we added to restore previous behaviour
+ added.forEach((ext) => delete extensions[ext]);
+ }
+ };
+
+ require: {
+ (id: string): any;
+ resolve: (id: string) => string;
+ ensure: () => void;
+ cache: typeof cache;
+ } = Object.assign(
+ (rawId: string) => {
+ const id = cookModuleId(rawId);
+ this.debug('require', id);
+ if (id in builtins) {
+ // The module is in the allowed list of builtin node modules
+ // Ideally we should prevent importing them, but webpack polyfills some
+ // So we check for the list of polyfills to determine which ones to support
+ if (builtins[id as keyof typeof builtins]) {
+ return require(id);
+ }
+
+ return null;
+ }
+
+ // Resolve module id (and filename) relatively to parent module
+ const filename = this.resolve(id);
+ if (filename === id && !path.isAbsolute(id)) {
+ // The module is a builtin node modules, but not in the allowed list
+ throw new Error(
+ `Unable to import "${id}". Importing Node builtins is not supported in the sandbox.`
+ );
+ }
+
+ this.dependencies?.push(id);
+
+ let cacheKey = filename;
+ let only: string[] = [];
+ if (this.imports?.has(id)) {
+ // We know what exactly we need from this module. Let's shake it!
+ only = this.imports.get(id)!.sort();
+ if (only.length === 0) {
+ // Probably the module is used as a value itself
+ // like `'The answer is ' + require('./module')`
+ only = ['default'];
+ }
+
+ cacheKey += `:${only.join(',')}`;
+ }
+
+ let m = cache[cacheKey];
+
+ if (!m) {
+ this.debug('cached:not-exist', id);
+ // Create the module if cached module is not available
+ m = new Module(filename, this.options, this.debuggerDepth + 1);
+ m.transform = this.transform;
+
+ // Store it in cache at this point with, otherwise
+ // we would end up in infinite loop with cyclic dependencies
+ cache[cacheKey] = m;
+
+ if (this.extensions.includes(path.extname(filename))) {
+ // To evaluate the file, we need to read it first
+ const code = fs.readFileSync(filename, 'utf-8');
+ if (/\.json$/.test(filename)) {
+ // For JSON files, parse it to a JS object similar to Node
+ m.exports = JSON.parse(code);
+ } else {
+ // For JS/TS files, evaluate the module
+ // The module will be transpiled using provided transform
+ m.evaluate(code, only.includes('*') ? null : only);
+ }
+ } else {
+ // For non JS/JSON requires, just export the id
+ // This is to support importing assets in webpack
+ // The module will be resolved by css-loader
+ m.exports = id;
+ }
+ } else {
+ this.debug('cached:exist', id);
+ }
+
+ return m.exports;
+ },
+ {
+ ensure: NOOP,
+ cache,
+ resolve: this.resolve,
+ }
+ );
+
+ evaluate(text: string, only: string[] | null = null) {
+ const filename = this.filename;
+ const matchedRules = this.options.rules
+ .filter(({ test }) => {
+ if (!test) {
+ return true;
+ }
+
+ if (typeof test === 'function') {
+ // this is not a test
+ // eslint-disable-next-line jest/no-disabled-tests
+ return test(filename);
+ }
+
+ if (test instanceof RegExp) {
+ return test.test(filename);
+ }
+
+ return false;
+ })
+ .reverse();
+
+ const cacheKey = [this.filename, ...(only ?? [])];
+
+ if (EvalCache.has(cacheKey, text)) {
+ this.exports = EvalCache.get(cacheKey, text);
+ return;
+ }
+
+ let code: string | null | undefined;
+ const action = matchedRules.length > 0 ? matchedRules[0].action : 'ignore';
+ if (action === 'ignore') {
+ this.debug('ignore', `${filename}`);
+ code = text;
+ } else {
+ // Action can be a function or a module name
+ const evaluator: Evaluator =
+ typeof action === 'function' ? action : require(action).default;
+
+ // For JavaScript files, we need to transpile it and to get the exports of the module
+ let imports: Module['imports'];
+
+ this.debug('prepare-evaluation', this.filename, 'using', evaluator.name);
+
+ [code, imports] = evaluator(this.filename, this.options, text, only);
+ this.imports = imports;
+
+ this.debug(
+ 'evaluate',
+ `${this.filename} (only ${(only || []).join(', ')}):\n${code}`
+ );
+ }
+
+ const script = new vm.Script(
+ `(function (exports) { ${code}\n})(exports);`,
+ {
+ filename: this.filename,
+ }
+ );
+
+ script.runInContext(
+ vm.createContext({
+ clearImmediate: NOOP,
+ clearInterval: NOOP,
+ clearTimeout: NOOP,
+ setImmediate: NOOP,
+ setInterval: NOOP,
+ setTimeout: NOOP,
+ global,
+ process,
+ module: this,
+ exports: this.exports,
+ require: this.require,
+ __filename: this.filename,
+ __dirname: path.dirname(this.filename),
+ })
+ );
+
+ EvalCache.set(cacheKey, text, this.exports);
+ }
+}
+
+Module.invalidate = () => {
+ cache = {};
+};
+
+Module.invalidateEvalCache = () => {
+ EvalCache.clear();
+};
+
+// Alias to resolve the module using node's resolve algorithm
+// This static property can be overriden by the webpack loader
+// This allows us to use webpack's module resolution algorithm
+Module._resolveFilename = (id, options) =>
+ (
+ NativeModule as unknown as {
+ _resolveFilename: (id: string, options: any) => string;
+ }
+ )._resolveFilename(id, options);
+
+Module._nodeModulePaths = (filename: string) =>
+ (
+ NativeModule as unknown as {
+ _nodeModulePaths: (filename: string) => string[];
+ }
+ )._nodeModulePaths(filename);
+
+export default Module;
diff --git a/@linaria/packages/babel/src/plugin-syntax-dynamic-import.d.ts b/@linaria/packages/babel/src/plugin-syntax-dynamic-import.d.ts
new file mode 100644
index 0000000..b7ddeed
--- /dev/null
+++ b/@linaria/packages/babel/src/plugin-syntax-dynamic-import.d.ts
@@ -0,0 +1,4 @@
+declare module '@babel/plugin-syntax-dynamic-import' {
+ const syntax: {};
+ export default syntax;
+}
diff --git a/@linaria/packages/babel/src/process.ts b/@linaria/packages/babel/src/process.ts
new file mode 100644
index 0000000..2553577
--- /dev/null
+++ b/@linaria/packages/babel/src/process.ts
@@ -0,0 +1,31 @@
+/**
+ * It contains API for mocked process variable available in node environment used to evaluate scripts with node's `vm` in ./module.ts
+ */
+export const nextTick = (fn: Function) => setTimeout(fn, 0);
+
+export const platform = 'browser';
+export const arch = 'browser';
+export const execPath = 'browser';
+export const title = 'browser';
+export const pid = 1;
+export const browser = true;
+export const argv = [];
+
+export const binding = function binding() {
+ throw new Error('No such module. (Possibly not yet loaded)');
+};
+
+export const cwd = () => '/';
+
+const noop = () => {};
+export const exit = noop;
+export const kill = noop;
+export const chdir = noop;
+export const umask = noop;
+export const dlopen = noop;
+export const uptime = noop;
+export const memoryUsage = noop;
+export const uvCounters = noop;
+export const features = {};
+
+export const env = process.env;
diff --git a/@linaria/packages/babel/src/transform.ts b/@linaria/packages/babel/src/transform.ts
new file mode 100644
index 0000000..cfc0ea6
--- /dev/null
+++ b/@linaria/packages/babel/src/transform.ts
@@ -0,0 +1,187 @@
+/**
+ * This file exposes transform function that:
+ * - parse the passed code to AST
+ * - transforms the AST using Linaria babel preset ('./babel/index.js) and additional config defined in Linaria config file or passed to bundler configuration.
+ * - runs generated CSS files through default of user-defined preprocessor
+ * - generates source maps for CSS files
+ * - return transformed code (without Linaria template literals), generated CSS, source maps and babel metadata from transform step.
+ */
+
+import path from 'path';
+import type { BabelFileMetadata, BabelFileResult } from '@babel/core';
+import { parseSync, transformFromAstSync } from '@babel/core';
+import stylis from 'stylis';
+import type { Mapping } from 'source-map';
+import { SourceMapGenerator } from 'source-map';
+import { debug } from '@linaria/logger';
+import loadOptions from './utils/loadOptions';
+import type { LinariaMetadata, Options, PreprocessorFn, Result } from './types';
+
+const STYLIS_DECLARATION = 1;
+const posixSep = path.posix.sep;
+const babelPreset = require.resolve('./index');
+
+export function transformUrl(
+ url: string,
+ outputFilename: string,
+ sourceFilename: string,
+ platformPath: typeof path = path
+) {
+ // Replace asset path with new path relative to the output CSS
+ const relative = platformPath.relative(
+ platformPath.dirname(outputFilename),
+ // Get the absolute path to the asset from the path relative to the JS file
+ platformPath.resolve(platformPath.dirname(sourceFilename), url)
+ );
+
+ if (platformPath.sep === posixSep) {
+ return relative;
+ }
+
+ return relative.split(platformPath.sep).join(posixSep);
+}
+
+export function shouldTransformCode(code: string): boolean {
+ return /\b(styled|css)/.test(code);
+}
+
+export function extractCssFromAst(
+ babelFileResult: BabelFileResult,
+ code: string,
+ options: Options
+): Result {
+ const { metadata, code: transformedCode, map } = babelFileResult;
+
+ if (
+ !metadata ||
+ !(metadata as BabelFileMetadata & { linaria: LinariaMetadata }).linaria
+ ) {
+ return {
+ code: transformedCode || '', // if there was only unused code we want to return transformed code which will be later removed by the bundler
+ sourceMap: map,
+ };
+ }
+
+ const { rules, replacements, dependencies } = (
+ metadata as BabelFileMetadata & {
+ linaria: LinariaMetadata;
+ }
+ ).linaria;
+ const mappings: Mapping[] = [];
+
+ let cssText = '';
+
+ let preprocessor: PreprocessorFn;
+ if (typeof options.preprocessor === 'function') {
+ // eslint-disable-next-line prefer-destructuring
+ preprocessor = options.preprocessor;
+ } else {
+ switch (options.preprocessor) {
+ case 'none':
+ preprocessor = (selector, text) => `${selector} {${text}}\n`;
+ break;
+ case 'stylis':
+ default:
+ stylis.use(null)((context, decl) => {
+ const { outputFilename } = options;
+ if (context === STYLIS_DECLARATION && outputFilename) {
+ // When writing to a file, we need to adjust the relative paths inside url(..) expressions
+ // It'll allow css-loader to resolve an imported asset properly
+ return decl.replace(
+ /\b(url\((["']?))(\.[^)]+?)(\2\))/g,
+ (match, p1, p2, p3, p4) =>
+ p1 + transformUrl(p3, outputFilename, options.filename) + p4
+ );
+ }
+
+ return decl;
+ });
+
+ preprocessor = stylis;
+ }
+ }
+
+ Object.keys(rules).forEach((selector, index) => {
+ mappings.push({
+ generated: {
+ line: index + 1,
+ column: 0,
+ },
+ original: rules[selector].start!,
+ name: selector,
+ source: '',
+ });
+
+ // Run each rule through stylis to support nesting
+ cssText += `${preprocessor(selector, rules[selector].cssText)}\n`;
+ });
+
+ return {
+ code: transformedCode || '',
+ cssText,
+ rules,
+ replacements,
+ dependencies,
+ sourceMap: map,
+
+ get cssSourceMapText() {
+ if (mappings?.length) {
+ const generator = new SourceMapGenerator({
+ file: options.filename.replace(/\.js$/, '.css'),
+ });
+
+ mappings.forEach((mapping) =>
+ generator.addMapping(
+ Object.assign({}, mapping, { source: options.filename })
+ )
+ );
+
+ generator.setSourceContent(options.filename, code);
+
+ return generator.toString();
+ }
+
+ return '';
+ },
+ };
+}
+
+export default function transform(code: string, options: Options): Result {
+ // Check if the file contains `css` or `styled` words first
+ // Otherwise we should skip transforming
+ if (!shouldTransformCode(code)) {
+ return {
+ code,
+ sourceMap: options.inputSourceMap,
+ };
+ }
+
+ debug(
+ 'transform',
+ `${options.filename} to ${options.outputFilename}\n${code}`
+ );
+
+ const pluginOptions = loadOptions(options.pluginOptions);
+ const babelOptions = pluginOptions?.babelOptions ?? null;
+
+ // Parse the code first so babel uses user's babel config for parsing
+ // We don't want to use user's config when transforming the code
+ const ast = parseSync(code, {
+ ...babelOptions,
+ filename: options.filename,
+ caller: { name: 'linaria' },
+ });
+
+ const babelFileResult = transformFromAstSync(ast!, code, {
+ ...(babelOptions?.rootMode ? { rootMode: babelOptions.rootMode } : null),
+ filename: options.filename,
+ presets: [[babelPreset, pluginOptions]],
+ babelrc: false,
+ configFile: false,
+ sourceMaps: true,
+ sourceFileName: options.filename,
+ inputSourceMap: options.inputSourceMap,
+ })!;
+
+ return extractCssFromAst(babelFileResult, code, options);
+}
diff --git a/@linaria/packages/babel/src/types.ts b/@linaria/packages/babel/src/types.ts
new file mode 100644
index 0000000..bee9a47
--- /dev/null
+++ b/@linaria/packages/babel/src/types.ts
@@ -0,0 +1,180 @@
+import type { Node, Expression, TaggedTemplateExpression } from '@babel/types';
+import type { TransformOptions } from '@babel/core';
+import type { NodePath } from '@babel/traverse';
+import type { StyledMeta } from '@linaria/core';
+import type { RawSourceMap } from 'source-map';
+import type { PluginOptions } from './utils/loadOptions';
+
+export type JSONValue = string | number | boolean | JSONObject | JSONArray;
+
+export interface JSONObject {
+ [x: string]: JSONValue;
+}
+
+export interface JSONArray extends Array<JSONValue> {}
+
+export type Serializable = JSONArray | JSONObject;
+
+export enum ValueType {
+ COMPONENT,
+ LAZY,
+ FUNCTION,
+ VALUE,
+}
+
+export type Value = Function | StyledMeta | string | number;
+
+export type ValueCache = Map<Expression | string, Value>;
+
+export type ComponentValue = {
+ kind: ValueType.COMPONENT;
+ ex: NodePath<Expression> | Expression | string;
+};
+
+export type LazyValue = {
+ kind: ValueType.LAZY;
+ ex: NodePath<Expression> | Expression | string;
+ originalEx: NodePath<Expression> | Expression | string;
+};
+
+export type FunctionValue = {
+ kind: ValueType.FUNCTION;
+ ex: any;
+};
+
+export type EvaluatedValue = {
+ kind: ValueType.VALUE;
+ value: Value;
+};
+
+export type ExpressionValue =
+ | ComponentValue
+ | LazyValue
+ | FunctionValue
+ | EvaluatedValue;
+
+export type TemplateExpression = {
+ styled?: { component: any };
+ path: NodePath<TaggedTemplateExpression>;
+ expressionValues: ExpressionValue[];
+};
+
+type Rules = {
+ [selector: string]: {
+ className: string;
+ displayName: string;
+ cssText: string;
+ start: Location | null | undefined;
+ };
+};
+
+type Replacements = Array<{
+ original: {
+ start: Location;
+ end: Location;
+ };
+ length: number;
+}>;
+
+type Dependencies = string[];
+
+export type State = {
+ queue: TemplateExpression[];
+ rules: Rules;
+ replacements: Replacements;
+ index: number;
+ dependencies: Dependencies;
+ file: {
+ opts: {
+ cwd: string;
+ root: string;
+ filename: string;
+ };
+ metadata: {
+ localName?: string;
+ linaria?: {
+ rules: Rules;
+ replacements: Replacements;
+ dependencies: Dependencies;
+ };
+ };
+ };
+};
+
+export type Evaluator = (
+ filename: string,
+ options: StrictOptions,
+ text: string,
+ only: string[] | null
+) => [string, Map<string, string[]> | null];
+
+export type EvalRule = {
+ test?: RegExp | ((path: string) => boolean);
+ action: Evaluator | 'ignore' | string;
+};
+
+type ClassNameFn = (hash: string, title: string) => string;
+
+export type StrictOptions = {
+ classNameSlug?: string | ClassNameFn;
+ displayName: boolean;
+ evaluate: boolean;
+ ignore?: RegExp;
+ babelOptions: TransformOptions;
+ rules: EvalRule[];
+};
+
+export type Location = {
+ line: number;
+ column: number;
+};
+
+export type Replacement = {
+ original: { start: Location; end: Location };
+ length: number;
+};
+
+export type Result = {
+ code: string;
+ sourceMap?: RawSourceMap | null;
+ cssText?: string;
+ cssSourceMapText?: string;
+ dependencies?: string[];
+ rules?: Rules;
+ replacements?: Replacement[];
+};
+
+export type LinariaMetadata = {
+ rules: Rules;
+ replacements: Replacement[];
+ dependencies: string[];
+};
+
+export type Options = {
+ filename: string;
+ preprocessor?: Preprocessor;
+ outputFilename?: string;
+ inputSourceMap?: RawSourceMap;
+ pluginOptions?: Partial<PluginOptions>;
+};
+
+export type PreprocessorFn = (selector: string, cssText: string) => string;
+export type Preprocessor = 'none' | 'stylis' | PreprocessorFn | void;
+
+type AllNodes = { [T in Node['type']]: Extract<Node, { type: T }> };
+
+declare module '@babel/types' {
+ type VisitorKeys = {
+ [T in keyof AllNodes]: Extract<
+ keyof AllNodes[T],
+ {
+ [Key in keyof AllNodes[T]]: AllNodes[T][Key] extends
+ | Node
+ | Node[]
+ | null
+ ? Key
+ : never;
+ }[keyof AllNodes[T]]
+ >;
+ };
+}
diff --git a/@linaria/packages/babel/src/units.ts b/@linaria/packages/babel/src/units.ts
new file mode 100644
index 0000000..7ac148a
--- /dev/null
+++ b/@linaria/packages/babel/src/units.ts
@@ -0,0 +1,101 @@
+// https://www.w3.org/TR/css-values-4/
+export const units = [
+ // font relative lengths
+ 'em',
+ 'ex',
+ 'cap',
+ 'ch',
+ 'ic',
+ 'rem',
+ 'lh',
+ 'rlh',
+
+ // viewport percentage lengths
+ 'vw',
+ 'vh',
+ 'vi',
+ 'vb',
+ 'vmin',
+ 'vmax',
+
+ // absolute lengths
+ 'cm',
+ 'mm',
+ 'Q',
+ 'in',
+ 'pc',
+ 'pt',
+ 'px',
+
+ // angle units
+ 'deg',
+ 'grad',
+ 'rad',
+ 'turn',
+
+ // duration units
+ 's',
+ 'ms',
+
+ // frequency units
+ 'Hz',
+ 'kHz',
+
+ // resolution units
+ 'dpi',
+ 'dpcm',
+ 'dppx',
+ 'x',
+
+ // https://www.w3.org/TR/css-grid-1/#fr-unit
+ 'fr',
+
+ // percentages
+ '%',
+];
+
+export const unitless = {
+ animationIterationCount: true,
+ borderImageOutset: true,
+ borderImageSlice: true,
+ borderImageWidth: true,
+ boxFlex: true,
+ boxFlexGroup: true,
+ boxOrdinalGroup: true,
+ columnCount: true,
+ columns: true,
+ flex: true,
+ flexGrow: true,
+ flexPositive: true,
+ flexShrink: true,
+ flexNegative: true,
+ flexOrder: true,
+ gridRow: true,
+ gridRowEnd: true,
+ gridRowSpan: true,
+ gridRowStart: true,
+ gridColumn: true,
+ gridColumnEnd: true,
+ gridColumnSpan: true,
+ gridColumnStart: true,
+ fontWeight: true,
+ lineClamp: true,
+ lineHeight: true,
+ opacity: true,
+ order: true,
+ orphans: true,
+ tabSize: true,
+ widows: true,
+ zIndex: true,
+ zoom: true,
+
+ // SVG-related properties
+ fillOpacity: true,
+ floodOpacity: true,
+ stopOpacity: true,
+ strokeDasharray: true,
+ strokeDashoffset: true,
+ strokeMiterlimit: true,
+ strokeOpacity: true,
+ strokeWidth: true,
+};
diff --git a/@linaria/packages/babel/src/utils/getLinariaComment.ts b/@linaria/packages/babel/src/utils/getLinariaComment.ts
new file mode 100644
index 0000000..06b3edb
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/getLinariaComment.ts
@@ -0,0 +1,31 @@
+import type { Node } from '@babel/types';
+
+const pattern = /^linaria (css|styled) (.+)$/;
+
+export default function getLinariaComment(
+ path: { node: Node },
+ remove: boolean = true
+): ['css' | 'styled' | null, ...(string | null)[]] {
+ const comments = path.node.leadingComments;
+ if (!comments) {
+ return [null, null, null, null];
+ }
+
+ const idx = comments.findIndex((comment) => pattern.test(comment.value));
+ if (idx === -1) {
+ return [null, null, null, null];
+ }
+
+ const matched = comments[idx].value.match(pattern);
+ if (!matched) {
+ return [null, null, null, null];
+ }
+
+ if (remove) {
+ path.node.leadingComments = comments.filter((_, i) => i !== idx);
+ }
+
+ const type = matched[1] === 'css' ? 'css' : 'styled';
+
+ return [type, ...matched[2].split(' ').map((i) => (i ? i : null))];
+}
diff --git a/@linaria/packages/babel/src/utils/getVisitorKeys.ts b/@linaria/packages/babel/src/utils/getVisitorKeys.ts
new file mode 100644
index 0000000..72f11f8
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/getVisitorKeys.ts
@@ -0,0 +1,10 @@
+import { types as t } from '@babel/core';
+import type { Node, VisitorKeys } from '@babel/types';
+
+type Keys<T extends Node> = (VisitorKeys[T['type']] & keyof T)[];
+
+export default function getVisitorKeys<TNode extends Node>(
+ node: TNode
+): Keys<TNode> {
+ return t.VISITOR_KEYS[node.type] as Keys<TNode>;
+}
diff --git a/@linaria/packages/babel/src/utils/hasImport.ts b/@linaria/packages/babel/src/utils/hasImport.ts
new file mode 100644
index 0000000..39c1f07
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/hasImport.ts
@@ -0,0 +1,83 @@
+import { dirname } from 'path';
+import Module from '../module';
+
+const linariaLibs = new Set([
+ '@linaria/core',
+ '@linaria/react',
+ 'linaria',
+ 'linaria/react',
+]);
+
+const safeResolve = (name: string) => {
+ try {
+ return require.resolve(name);
+ } catch (err) {
+ return null;
+ }
+};
+
+// Verify if the binding is imported from the specified source
+export default function hasImport(
+ t: any,
+ scope: any,
+ filename: string,
+ identifier: string,
+ sources: string[]
+): boolean {
+ const binding = scope.getAllBindings()[identifier];
+
+ if (!binding) {
+ return false;
+ }
+
+ const p = binding.path;
+
+ const resolveFromFile = (id: string) => {
+ try {
+ return Module._resolveFilename(id, {
+ id: filename,
+ filename,
+ paths: Module._nodeModulePaths(dirname(filename)),
+ });
+ } catch (e) {
+ return null;
+ }
+ };
+
+ const isImportingModule = (value: string) =>
+ sources.some(
+ (source) =>
+ // If the value is an exact match, assume it imports the module
+ value === source ||
+ // Otherwise try to resolve both and check if they are the same file
+ resolveFromFile(value) ===
+ (linariaLibs.has(source)
+ ? safeResolve(source)
+ : resolveFromFile(source))
+ );
+
+ if (t.isImportSpecifier(p) && t.isImportDeclaration(p.parentPath)) {
+ return isImportingModule(p.parentPath.node.source.value);
+ }
+
+ if (t.isVariableDeclarator(p)) {
+ if (
+ t.isCallExpression(p.node.init) &&
+ t.isIdentifier(p.node.init.callee) &&
+ p.node.init.callee.name === 'require' &&
+ p.node.init.arguments.length === 1
+ ) {
+ const node = p.node.init.arguments[0];
+
+ if (t.isStringLiteral(node)) {
+ return isImportingModule(node.value);
+ }
+
+ if (t.isTemplateLiteral(node) && node.quasis.length === 1) {
+ return isImportingModule(node.quasis[0].value.cooked);
+ }
+ }
+ }
+
+ return false;
+}
diff --git a/@linaria/packages/babel/src/utils/isBoxedPrimitive.ts b/@linaria/packages/babel/src/utils/isBoxedPrimitive.ts
new file mode 100644
index 0000000..2d20eec
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/isBoxedPrimitive.ts
@@ -0,0 +1,10 @@
+// There is a problem with using boxed numbers and strings in TS,
+// so we cannot just use `instanceof` here
+
+const constructors = ['Number', 'String'];
+export default function isBoxedPrimitive(o: any): o is Number | String {
+ return (
+ constructors.includes(o.constructor.name) &&
+ typeof o?.valueOf() !== 'object'
+ );
+}
diff --git a/@linaria/packages/babel/src/utils/isNode.ts b/@linaria/packages/babel/src/utils/isNode.ts
new file mode 100644
index 0000000..2340ef9
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/isNode.ts
@@ -0,0 +1,5 @@
+import type { Node } from '@babel/types';
+
+const isNode = (obj: any): obj is Node => obj?.type !== undefined;
+
+export default isNode;
diff --git a/@linaria/packages/babel/src/utils/isSerializable.ts b/@linaria/packages/babel/src/utils/isSerializable.ts
new file mode 100644
index 0000000..1f65d58
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/isSerializable.ts
@@ -0,0 +1,11 @@
+import type { Serializable } from '../types';
+import isBoxedPrimitive from './isBoxedPrimitive';
+
+export default function isSerializable(o: any): o is Serializable {
+ return (
+ (Array.isArray(o) && o.every(isSerializable)) ||
+ (typeof o === 'object' &&
+ o !== null &&
+ (o.constructor.name === 'Object' || isBoxedPrimitive(o)))
+ );
+}
diff --git a/@linaria/packages/babel/src/utils/isStyledOrCss.ts b/@linaria/packages/babel/src/utils/isStyledOrCss.ts
new file mode 100644
index 0000000..04e4789
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/isStyledOrCss.ts
@@ -0,0 +1,67 @@
+import type {
+ CallExpression,
+ Expression,
+ TaggedTemplateExpression,
+} from '@babel/types';
+import type { NodePath } from '@babel/traverse';
+import type { State, TemplateExpression } from '../types';
+import { Core } from '../babel';
+import hasImport from './hasImport';
+
+type Result = NonNullable<TemplateExpression['styled']> | 'css' | null;
+
+const cache = new WeakMap<NodePath<TaggedTemplateExpression>, Result>();
+
+export default function isStyledOrCss(
+ { types: t }: Core,
+ path: NodePath<TaggedTemplateExpression>,
+ state: State
+): Result {
+ if (!cache.has(path)) {
+ const { tag } = path.node;
+
+ const localName = state.file.metadata.localName || 'styled';
+
+ if (
+ t.isCallExpression(tag) &&
+ t.isIdentifier(tag.callee) &&
+ tag.arguments.length === 1 &&
+ tag.callee.name === localName &&
+ hasImport(t, path.scope, state.file.opts.filename, localName, [
+ '@linaria/react',
+ 'linaria/react',
+ ])
+ ) {
+ const tagPath = path.get('tag') as NodePath<CallExpression>;
+ cache.set(path, {
+ component: tagPath.get('arguments')[0] as NodePath<Expression>,
+ });
+ } else if (
+ t.isMemberExpression(tag) &&
+ t.isIdentifier(tag.object) &&
+ t.isIdentifier(tag.property) &&
+ tag.object.name === localName &&
+ hasImport(t, path.scope, state.file.opts.filename, localName, [
+ '@linaria/react',
+ 'linaria/react',
+ ])
+ ) {
+ cache.set(path, {
+ component: { node: t.stringLiteral(tag.property.name) },
+ });
+ } else if (
+ hasImport(t, path.scope, state.file.opts.filename, 'css', [
+ '@linaria/core',
+ 'linaria',
+ ]) &&
+ t.isIdentifier(tag) &&
+ tag.name === 'css'
+ ) {
+ cache.set(path, 'css');
+ } else {
+ cache.set(path, null);
+ }
+ }
+
+ return cache.get(path) ?? null;
+}
diff --git a/@linaria/packages/babel/src/utils/loadOptions.ts b/@linaria/packages/babel/src/utils/loadOptions.ts
new file mode 100644
index 0000000..8c0f0b8
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/loadOptions.ts
@@ -0,0 +1,38 @@
+import cosmiconfig from 'cosmiconfig';
+import type { StrictOptions } from '../types';
+
+export type PluginOptions = StrictOptions & {
+ configFile?: string;
+};
+
+const explorer = cosmiconfig('linaria');
+
+export default function loadOptions(
+ overrides: Partial<PluginOptions> = {}
+): Partial<StrictOptions> {
+ const { configFile, ignore, ...rest } = overrides;
+
+ const result =
+ configFile !== undefined
+ ? explorer.loadSync(configFile)
+ : explorer.searchSync();
+
+ return {
+ displayName: false,
+ evaluate: true,
+ rules: [
+ {
+ // FIXME: if `rule` is not specified in a config, `@linaria/shaker` should be added as a dependency
+ // eslint-disable-next-line import/no-extraneous-dependencies
+ action: require('@linaria/shaker').default,
+ },
+ {
+ // The old `ignore` option is used as a default value for `ignore` rule.
+ test: ignore ?? /[\\/]node_modules[\\/]/,
+ action: 'ignore',
+ },
+ ],
+ ...(result ? result.config : null),
+ ...rest,
+ };
+}
diff --git a/@linaria/packages/babel/src/utils/peek.ts b/@linaria/packages/babel/src/utils/peek.ts
new file mode 100644
index 0000000..cabadc4
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/peek.ts
@@ -0,0 +1,3 @@
+const peek = <T>(stack: T[], offset = 1): T => stack[stack.length - offset];
+
+export default peek;
diff --git a/@linaria/packages/babel/src/utils/slugify.ts b/@linaria/packages/babel/src/utils/slugify.ts
new file mode 100644
index 0000000..39772ad
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/slugify.ts
@@ -0,0 +1,83 @@
+/**
+ * This file contains a utility to generate hashes to be used as generated class names
+ */
+
+/* eslint-disable no-bitwise, default-case, no-param-reassign, prefer-destructuring */
+
+/**
+ * murmurhash2 via https://gist.github.com/raycmorgan/588423
+ */
+
+function doHash(str: string, seed: number = 0) {
+ const m = 0x5bd1e995;
+ const r = 24;
+ let h = seed ^ str.length;
+ let length = str.length;
+ let currentIndex = 0;
+
+ while (length >= 4) {
+ let k = UInt32(str, currentIndex);
+
+ k = Umul32(k, m);
+ k ^= k >>> r;
+ k = Umul32(k, m);
+
+ h = Umul32(h, m);
+ h ^= k;
+
+ currentIndex += 4;
+ length -= 4;
+ }
+
+ switch (length) {
+ case 3:
+ h ^= UInt16(str, currentIndex);
+ h ^= str.charCodeAt(currentIndex + 2) << 16;
+ h = Umul32(h, m);
+ break;
+
+ case 2:
+ h ^= UInt16(str, currentIndex);
+ h = Umul32(h, m);
+ break;
+
+ case 1:
+ h ^= str.charCodeAt(currentIndex);
+ h = Umul32(h, m);
+ break;
+ }
+
+ h ^= h >>> 13;
+ h = Umul32(h, m);
+ h ^= h >>> 15;
+
+ return h >>> 0;
+}
+
+function UInt32(str: string, pos: number) {
+ return (
+ str.charCodeAt(pos++) +
+ (str.charCodeAt(pos++) << 8) +
+ (str.charCodeAt(pos++) << 16) +
+ (str.charCodeAt(pos) << 24)
+ );
+}
+
+function UInt16(str: string, pos: number) {
+ return str.charCodeAt(pos++) + (str.charCodeAt(pos++) << 8);
+}
+
+function Umul32(n: number, m: number) {
+ n |= 0;
+ m |= 0;
+ const nlo = n & 0xffff;
+ const nhi = n >>> 16;
+ const res = (nlo * m + (((nhi * m) & 0xffff) << 16)) | 0;
+ return res;
+}
+
+function slugify(code: string) {
+ return doHash(code).toString(36);
+}
+
+export default slugify;
diff --git a/@linaria/packages/babel/src/utils/stripLines.ts b/@linaria/packages/babel/src/utils/stripLines.ts
new file mode 100644
index 0000000..d0495ae
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/stripLines.ts
@@ -0,0 +1,23 @@
+import type { Location } from '../types';
+
+// Stripping away the new lines ensures that we preserve line numbers
+// This is useful in case of tools such as the stylelint pre-processor
+// This should be safe because strings cannot contain newline: https://www.w3.org/TR/CSS2/syndata.html#strings
+export default function stripLines(
+ loc: { start: Location; end: Location },
+ text: string | number
+) {
+ let result = String(text)
+ .replace(/[\r\n]+/g, ' ')
+ .trim();
+
+ // If the start and end line numbers aren't same, add new lines to span the text across multiple lines
+ if (loc.start.line !== loc.end.line) {
+ result += '\n'.repeat(loc.end.line - loc.start.line);
+
+ // Add extra spaces to offset the column
+ result += ' '.repeat(loc.end.column);
+ }
+
+ return result;
+}
diff --git a/@linaria/packages/babel/src/utils/throwIfInvalid.ts b/@linaria/packages/babel/src/utils/throwIfInvalid.ts
new file mode 100644
index 0000000..a883f9f
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/throwIfInvalid.ts
@@ -0,0 +1,49 @@
+import generator from '@babel/generator';
+import type { Serializable } from '../types';
+import isSerializable from './isSerializable';
+
+// Throw if we can't handle the interpolated value
+function throwIfInvalid(
+ value: Error | Function | string | number | Serializable | undefined,
+ ex: any
+): void {
+ if (
+ typeof value === 'function' ||
+ typeof value === 'string' ||
+ (typeof value === 'number' && Number.isFinite(value)) ||
+ isSerializable(value)
+ ) {
+ return;
+ }
+
+ // We can't use instanceof here so let's use duck typing
+ if (value && typeof value !== 'number' && value.stack && value.message) {
+ throw ex.buildCodeFrameError(
+ `An error occurred when evaluating the expression:
+
+ > ${value.message}.
+
+ 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.
+ `
+ );
+ }
+
+ const stringified =
+ typeof value === 'object' ? JSON.stringify(value) : String(value);
+
+ throw ex.buildCodeFrameError(
+ `The expression evaluated to '${stringified}', 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(${
+ generator(ex.node).code
+ })'.`
+ );
+}
+
+export default throwIfInvalid;
diff --git a/@linaria/packages/babel/src/utils/toCSS.ts b/@linaria/packages/babel/src/utils/toCSS.ts
new file mode 100644
index 0000000..b396087
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/toCSS.ts
@@ -0,0 +1,57 @@
+import { unitless } from '../units';
+import type { JSONValue } from '../types';
+import isSerializable from './isSerializable';
+import isBoxedPrimitive from './isBoxedPrimitive';
+
+const hyphenate = (s: string) => {
+ if (s.startsWith('--')) {
+ // It's a custom property which is already well formatted.
+ return s;
+ }
+ return (
+ s
+ // Hyphenate CSS property names from camelCase version from JS string
+ .replace(/([A-Z])/g, (match, p1) => `-${p1.toLowerCase()}`)
+ // Special case for `-ms` because in JS it starts with `ms` unlike `Webkit`
+ .replace(/^ms-/, '-ms-')
+ );
+};
+
+// Some tools such as polished.js output JS objects
+// To support them transparently, we convert JS objects to CSS strings
+export default function toCSS(o: JSONValue): string {
+ if (Array.isArray(o)) {
+ return o.map(toCSS).join('\n');
+ }
+
+ if (isBoxedPrimitive(o)) {
+ return o.valueOf().toString();
+ }
+
+ return Object.entries(o)
+ .filter(
+ ([, value]) =>
+ // Ignore all falsy values except numbers
+ typeof value === 'number' || value
+ )
+ .map(([key, value]) => {
+ if (isSerializable(value)) {
+ return `${key} { ${toCSS(value)} }`;
+ }
+
+ return `${hyphenate(key)}: ${
+ typeof value === 'number' &&
+ value !== 0 &&
+ // Strip vendor prefixes when checking if the value is unitless
+ !(
+ key.replace(
+ /^(Webkit|Moz|O|ms)([A-Z])(.+)$/,
+ (match, p1, p2, p3) => `${p2.toLowerCase()}${p3}`
+ ) in unitless
+ )
+ ? `${value}px`
+ : value
+ };`;
+ })
+ .join(' ');
+}
diff --git a/@linaria/packages/babel/src/utils/toValidCSSIdentifier.ts b/@linaria/packages/babel/src/utils/toValidCSSIdentifier.ts
new file mode 100644
index 0000000..9a2be8f
--- /dev/null
+++ b/@linaria/packages/babel/src/utils/toValidCSSIdentifier.ts
@@ -0,0 +1,3 @@
+export default function toValidCSSIdentifier(s: string) {
+ return s.replace(/[^-_a-z0-9\u00A0-\uFFFF]/gi, '_').replace(/^\d/, '_');
+}
diff --git a/@linaria/packages/babel/src/visitors/CollectDependencies.ts b/@linaria/packages/babel/src/visitors/CollectDependencies.ts
new file mode 100644
index 0000000..fc6d2fd
--- /dev/null
+++ b/@linaria/packages/babel/src/visitors/CollectDependencies.ts
@@ -0,0 +1,132 @@
+/**
+ * This file is a visitor that checks TaggedTemplateExpressions and look for Linaria css or styled templates.
+ * For each template it makes a list of dependencies, try to evaluate expressions, and if it is not possible, mark them as lazy dependencies.
+ */
+
+import type {
+ Expression,
+ Identifier as IdentifierNode,
+ TaggedTemplateExpression,
+ TSType,
+} from '@babel/types';
+import type { NodePath } from '@babel/traverse';
+import { debug } from '@linaria/logger';
+import generator from '@babel/generator';
+import throwIfInvalid from '../utils/throwIfInvalid';
+import type { State, StrictOptions, ExpressionValue } from '../types';
+import { ValueType } from '../types';
+import isStyledOrCss from '../utils/isStyledOrCss';
+import { Core } from '../babel';
+
+/**
+ * Hoist the node and its dependencies to the highest scope possible
+ */
+function hoist(babel: Core, ex: NodePath<Expression | null>) {
+ const Identifier = (idPath: NodePath<IdentifierNode>) => {
+ if (!idPath.isReferencedIdentifier()) {
+ return;
+ }
+ const binding = idPath.scope.getBinding(idPath.node.name);
+ if (!binding) return;
+ const { scope, path: bindingPath, referencePaths } = binding;
+ // parent here can be null or undefined in different versions of babel
+ if (!scope.parent) {
+ // It's a variable from global scope
+ return;
+ }
+
+ if (bindingPath.isVariableDeclarator()) {
+ const initPath = bindingPath.get('init') as NodePath<Expression | null>;
+ hoist(babel, initPath);
+ initPath.hoist(scope);
+ if (initPath.isIdentifier()) {
+ referencePaths.forEach((referencePath) => {
+ referencePath.replaceWith(babel.types.identifier(initPath.node.name));
+ });
+ }
+ }
+ };
+
+ if (ex.isIdentifier()) {
+ return Identifier(ex);
+ }
+
+ ex.traverse({
+ Identifier,
+ });
+}
+
+export default function CollectDependencies(
+ babel: Core,
+ path: NodePath<TaggedTemplateExpression>,
+ state: State,
+ options: StrictOptions
+) {
+ const { types: t } = babel;
+ const styledOrCss = isStyledOrCss(babel, path, state);
+ if (!styledOrCss) {
+ return;
+ }
+ const expressions = path.get('quasi').get('expressions');
+
+ debug('template-parse:identify-expressions', expressions.length);
+
+ const expressionValues: ExpressionValue[] = expressions.map(
+ (ex: NodePath<Expression | TSType>) => {
+ if (!ex.isExpression()) {
+ throw ex.buildCodeFrameError(
+ `The expression '${generator(ex.node).code}' is not supported.`
+ );
+ }
+
+ const result = ex.evaluate();
+ if (result.confident) {
+ throwIfInvalid(result.value, ex);
+ return { kind: ValueType.VALUE, value: result.value };
+ }
+ if (
+ options.evaluate &&
+ !(t.isFunctionExpression(ex) || t.isArrowFunctionExpression(ex))
+ ) {
+ // save original expression that may be changed during hoisting
+ const originalExNode = t.cloneNode(ex.node);
+
+ hoist(babel, ex as NodePath<Expression | null>);
+
+ // save hoisted expression to be used to evaluation
+ const hoistedExNode = t.cloneNode(ex.node);
+
+ // get back original expression to the tree
+ ex.replaceWith(originalExNode);
+
+ return { kind: ValueType.LAZY, ex: hoistedExNode, originalEx: ex };
+ }
+
+ return { kind: ValueType.FUNCTION, ex };
+ }
+ );
+
+ debug(
+ 'template-parse:evaluate-expressions',
+ expressionValues.map((expressionValue) =>
+ expressionValue.kind === ValueType.VALUE ? expressionValue.value : 'lazy'
+ )
+ );
+
+ if (styledOrCss !== 'css' && 'name' in styledOrCss.component.node) {
+ // It's not a real dependency.
+ // It can be simplified because we need just a className.
+ expressionValues.push({
+ // kind: ValueType.COMPONENT,
+ kind: ValueType.LAZY,
+ ex: styledOrCss.component.node.name,
+ originalEx: styledOrCss.component.node.name,
+ });
+ }
+
+ state.queue.push({
+ styled: styledOrCss !== 'css' ? styledOrCss : undefined,
+ path,
+ expressionValues,
+ });
+}
diff --git a/@linaria/packages/babel/src/visitors/DetectStyledImportName.ts b/@linaria/packages/babel/src/visitors/DetectStyledImportName.ts
new file mode 100644
index 0000000..07bf724
--- /dev/null
+++ b/@linaria/packages/babel/src/visitors/DetectStyledImportName.ts
@@ -0,0 +1,33 @@
+/* eslint-disable no-param-reassign */
+/**
+ * This Visitor checks if import of `@linaria/react` was renamed and stores that information in state
+ */
+
+import type { ImportDeclaration } from '@babel/types';
+import type { NodePath } from '@babel/traverse';
+import type { State } from '../types';
+import { Core } from '../babel';
+
+export default function DetectStyledImportName(
+ { types: t }: Core,
+ path: NodePath<ImportDeclaration>,
+ state: State
+) {
+ if (!t.isLiteral(path.node.source, { value: '@linaria/react' })) {
+ return;
+ }
+
+ path.node.specifiers.forEach((specifier) => {
+ if (!t.isImportSpecifier(specifier)) {
+ return;
+ }
+
+ const importedName = t.isStringLiteral(specifier.imported)
+ ? specifier.imported.value
+ : specifier.imported.name;
+
+ if (specifier.local.name !== importedName) {
+ state.file.metadata.localName = specifier.local.name;
+ }
+ });
+}
diff --git a/@linaria/packages/babel/src/visitors/GenerateClassNames.ts b/@linaria/packages/babel/src/visitors/GenerateClassNames.ts
new file mode 100644
index 0000000..9bebf2e
--- /dev/null
+++ b/@linaria/packages/babel/src/visitors/GenerateClassNames.ts
@@ -0,0 +1,161 @@
+/**
+ * This file is a visitor that checks TaggedTemplateExpressions and look for Linaria css or styled templates.
+ * For each template it generates a slug that will be used as a CSS class for particular Template Expression,
+ * and generates a display name for class or styled components.
+ * It saves that meta data as comment above the template, to be later used in templateProcessor.
+ */
+
+import { basename, dirname, relative } from 'path';
+import type { ObjectProperty, TaggedTemplateExpression } from '@babel/types';
+import type { NodePath } from '@babel/traverse';
+import { debug } from '@linaria/logger';
+import type { State, StrictOptions } from '../types';
+import toValidCSSIdentifier from '../utils/toValidCSSIdentifier';
+import slugify from '../utils/slugify';
+import getLinariaComment from '../utils/getLinariaComment';
+import isStyledOrCss from '../utils/isStyledOrCss';
+import { Core } from '../babel';
+
+export default function GenerateClassNames(
+ babel: Core,
+ path: NodePath<TaggedTemplateExpression>,
+ state: State,
+ options: StrictOptions
+) {
+ const { types: t } = babel;
+ const styledOrCss = isStyledOrCss(babel, path, state);
+ if (!styledOrCss) {
+ return;
+ }
+
+ const expressions = path.get('quasi').get('expressions');
+
+ debug('template-parse:identify-expressions', expressions.length);
+
+ // Increment the index of the style we're processing
+ // This is used for slug generation to prevent collision
+ // Also used for display name if it couldn't be determined
+ state.index++;
+
+ let [, slug, displayName, predefinedClassName] = getLinariaComment(path);
+
+ const parent = path.findParent(
+ (p) =>
+ t.isObjectProperty(p) ||
+ t.isJSXOpeningElement(p) ||
+ t.isVariableDeclarator(p)
+ );
+
+ if (!displayName && parent) {
+ const parentNode = parent.node;
+ if (t.isObjectProperty(parentNode)) {
+ if ('name' in parentNode.key) {
+ displayName = parentNode.key.name;
+ } else if ('value' in parentNode.key) {
+ displayName = parentNode.key.value.toString();
+ } else {
+ const keyPath = (parent as NodePath<ObjectProperty>).get('key');
+ displayName = keyPath.getSource();
+ }
+ } else if (
+ t.isJSXOpeningElement(parentNode) &&
+ t.isJSXIdentifier(parentNode.name)
+ ) {
+ displayName = parentNode.name.name;
+ } else if (
+ t.isVariableDeclarator(parentNode) &&
+ t.isIdentifier(parentNode.id)
+ ) {
+ displayName = parentNode.id.name;
+ }
+ }
+
+ if (!displayName) {
+ // Try to derive the path from the filename
+ displayName = basename(state.file.opts.filename);
+
+ if (/^index\.[a-z0-9]+$/.test(displayName)) {
+ // If the file name is 'index', better to get name from parent folder
+ displayName = basename(dirname(state.file.opts.filename));
+ }
+
+ // Remove the file extension
+ displayName = displayName.replace(/\.[a-z0-9]+$/, '');
+
+ if (displayName) {
+ displayName += state.index;
+ } else {
+ throw path.buildCodeFrameError(
+ "Couldn't determine a name for the component. Ensure that it's either:\n" +
+ '- Assigned to a variable\n' +
+ '- Is an object property\n' +
+ '- Is a prop in a JSX element\n'
+ );
+ }
+ }
+
+ // Custom properties need to start with a letter, so we prefix the slug
+ // Also use append the index of the class to the filename for uniqueness in the file
+ slug =
+ slug ||
+ toValidCSSIdentifier(
+ `${displayName.charAt(0).toLowerCase()}${slugify(
+ `${relative(state.file.opts.root, state.file.opts.filename)}:${
+ state.index
+ }`
+ )}`
+ );
+
+ let className = predefinedClassName
+ ? predefinedClassName
+ : options.displayName
+ ? `${toValidCSSIdentifier(displayName!)}_${slug!}`
+ : slug!;
+
+ // The className can be defined by the user either as fn or a string
+ if (typeof options.classNameSlug === 'function') {
+ try {
+ className = toValidCSSIdentifier(
+ options.classNameSlug(slug, displayName)
+ );
+ } catch {
+ throw new Error(`classNameSlug option must return a string`);
+ }
+ }
+
+ if (typeof options.classNameSlug === 'string') {
+ const { classNameSlug } = options;
+
+ // Available variables for the square brackets used in `classNameSlug` options
+ const classNameSlugVars: Record<string, string | null> = {
+ hash: slug,
+ title: displayName,
+ };
+
+ // Variables that were used in the config for `classNameSlug`
+ const optionVariables = classNameSlug.match(/\[.*?]/g) || [];
+ let cnSlug = classNameSlug;
+
+ for (let i = 0, l = optionVariables.length; i < l; i++) {
+ const v = optionVariables[i].slice(1, -1); // Remove the brackets around the variable name
+
+ // Replace the var if it key and value exist otherwise place an empty string
+ cnSlug = cnSlug.replace(`[${v}]`, classNameSlugVars[v] || '');
+ }
+
+ className = toValidCSSIdentifier(cnSlug);
+ }
+
+ const type = styledOrCss === 'css' ? 'css' : 'styled';
+
+ debug(
+ `template-parse:generated-meta:${type}`,
+ `slug: ${slug}, displayName: ${displayName}, className: ${className}`
+ );
+
+ // Save evaluated slug and displayName for future usage in templateProcessor
+ path.addComment(
+ 'leading',
+ `linaria ${type} ${slug} ${displayName} ${className}`
+ );
+}
diff --git a/@linaria/packages/babel/tsconfig.json b/@linaria/packages/babel/tsconfig.json
new file mode 100644
index 0000000..73092b0
--- /dev/null
+++ b/@linaria/packages/babel/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "paths": {},
+ "rootDir": "src/"
+ },
+ "references": [{ "path": "../core" }, { "path": "../logger" }]
+}