summaryrefslogtreecommitdiff
path: root/@linaria/packages/shaker
diff options
context:
space:
mode:
Diffstat (limited to '@linaria/packages/shaker')
-rw-r--r--@linaria/packages/shaker/CHANGELOG.md88
-rw-r--r--@linaria/packages/shaker/README.md35
-rw-r--r--@linaria/packages/shaker/__fixtures__/bar.js2
-rw-r--r--@linaria/packages/shaker/__fixtures__/foo.js2
-rw-r--r--@linaria/packages/shaker/__fixtures__/reexports.js2
-rw-r--r--@linaria/packages/shaker/__tests__/__snapshots__/shaker.test.ts.snap1229
-rw-r--r--@linaria/packages/shaker/__tests__/shaker.test.ts100
-rw-r--r--@linaria/packages/shaker/babel.config.js3
-rw-r--r--@linaria/packages/shaker/package.json59
-rw-r--r--@linaria/packages/shaker/src/DepsGraph.ts149
-rw-r--r--@linaria/packages/shaker/src/GraphBuilderState.ts45
-rw-r--r--@linaria/packages/shaker/src/Visitors.ts87
-rw-r--r--@linaria/packages/shaker/src/dumpNode.ts63
-rw-r--r--@linaria/packages/shaker/src/graphBuilder.ts192
-rw-r--r--@linaria/packages/shaker/src/identifierHandlers.ts130
-rw-r--r--@linaria/packages/shaker/src/index.ts65
-rw-r--r--@linaria/packages/shaker/src/langs/core.ts653
-rw-r--r--@linaria/packages/shaker/src/scope.ts210
-rw-r--r--@linaria/packages/shaker/src/shaker.ts127
-rw-r--r--@linaria/packages/shaker/src/types.ts22
-rw-r--r--@linaria/packages/shaker/tsconfig.json9
21 files changed, 3272 insertions, 0 deletions
diff --git a/@linaria/packages/shaker/CHANGELOG.md b/@linaria/packages/shaker/CHANGELOG.md
new file mode 100644
index 0000000..62ecee4
--- /dev/null
+++ b/@linaria/packages/shaker/CHANGELOG.md
@@ -0,0 +1,88 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [3.0.0-beta.11](https://github.com/callstack/linaria/compare/v3.0.0-beta.10...v3.0.0-beta.11) (2021-08-08)
+
+
+### Bug Fixes
+
+* **shaker:** string literals for addressing values in imported NS ([#815](https://github.com/callstack/linaria/issues/815)) ([8adf43e](https://github.com/callstack/linaria/commit/8adf43ec56b107c8017608ccc460d0ba8794c8ef))
+
+
+
+
+
+# [3.0.0-beta.10](https://github.com/callstack/linaria/compare/v3.0.0-beta.9...v3.0.0-beta.10) (2021-07-24)
+
+
+### Bug Fixes
+
+* **shaker:** use the last export statement instead of the 1st ([#804](https://github.com/callstack/linaria/issues/804)) ([b79584c](https://github.com/callstack/linaria/commit/b79584c292aaa50eb2a420b73434341419b01ff9))
+
+
+
+
+
+# [3.0.0-beta.9](https://github.com/callstack/linaria/compare/v3.0.0-beta.8...v3.0.0-beta.9) (2021-07-23)
+
+
+### Bug Fixes
+
+* **shaker:** keep exports if there are dependent code (fixes [#804](https://github.com/callstack/linaria/issues/804)) ([#807](https://github.com/callstack/linaria/issues/807)) ([4bb7744](https://github.com/callstack/linaria/commit/4bb77444a604581877a64d6f88dfac0bc04583f0))
+* **shaker:** support for "export * from …" ([#809](https://github.com/callstack/linaria/issues/809)) ([b06c1ba](https://github.com/callstack/linaria/commit/b06c1ba5f44ae7af23cf7793c13a2acfee1bf706))
+
+
+
+
+
+# [3.0.0-beta.8](https://github.com/callstack/linaria/compare/v3.0.0-beta.7...v3.0.0-beta.8) (2021-07-18)
+
+**Note:** Version bump only for package @linaria/shaker
+
+
+
+
+
+# [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/shaker
+
+
+
+
+
+# [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/shaker
+
+
+
+
+
+# [3.0.0-beta.2](https://github.com/callstack/linaria/compare/v3.0.0-beta.1...v3.0.0-beta.2) (2021-04-11)
+
+**Note:** Version bump only for package @linaria/shaker
diff --git a/@linaria/packages/shaker/README.md b/@linaria/packages/shaker/README.md
new file mode 100644
index 0000000..0d75b37
--- /dev/null
+++ b/@linaria/packages/shaker/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/shaker/__fixtures__/bar.js b/@linaria/packages/shaker/__fixtures__/bar.js
new file mode 100644
index 0000000..80afd18
--- /dev/null
+++ b/@linaria/packages/shaker/__fixtures__/bar.js
@@ -0,0 +1,2 @@
+export const bar1 = 'bar1';
+export const bar2 = 'bar2';
diff --git a/@linaria/packages/shaker/__fixtures__/foo.js b/@linaria/packages/shaker/__fixtures__/foo.js
new file mode 100644
index 0000000..d558031
--- /dev/null
+++ b/@linaria/packages/shaker/__fixtures__/foo.js
@@ -0,0 +1,2 @@
+export const foo1 = "foo1";
+export const foo2 = "foo2";
diff --git a/@linaria/packages/shaker/__fixtures__/reexports.js b/@linaria/packages/shaker/__fixtures__/reexports.js
new file mode 100644
index 0000000..88f8d33
--- /dev/null
+++ b/@linaria/packages/shaker/__fixtures__/reexports.js
@@ -0,0 +1,2 @@
+export * from './foo';
+export * from './bar';
diff --git a/@linaria/packages/shaker/__tests__/__snapshots__/shaker.test.ts.snap b/@linaria/packages/shaker/__tests__/__snapshots__/shaker.test.ts.snap
new file mode 100644
index 0000000..b14a18b
--- /dev/null
+++ b/@linaria/packages/shaker/__tests__/__snapshots__/shaker.test.ts.snap
@@ -0,0 +1,1229 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`shaker derives display name from filename 1`] = `
+"import { styled } from '@linaria/react';
+export default /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"FancyName0\\",
+ class: \\"FancyName0_f1tjvuuv\\"
+});"
+`;
+
+exports[`shaker derives display name from filename 2`] = `
+
+CSS:
+
+.FancyName0_f1tjvuuv {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker derives display name from parent folder name 1`] = `
+"import { styled } from '@linaria/react';
+export default /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"FancyName0\\",
+ class: \\"FancyName0_fud4l0y\\"
+});"
+`;
+
+exports[`shaker derives display name from parent folder name 2`] = `
+
+CSS:
+
+.FancyName0_fud4l0y {
+ font-size: 14px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker does not strip instanbul coverage sequences 1`] = `
+"var cov_2dr9r1nt95 = function () {
+ var path = \\"/home/user/project/file.js\\";
+ var hash = \\"23f56bbaeb2ebcd213f41374d3b2af1bce287bd3\\";
+ var global = new Function(\\"return this\\")();
+ var gcv = \\"__coverage__\\";
+ var coverageData = {
+ path: \\"/home/user/project/file.js\\",
+ statementMap: {
+ \\"0\\": {
+ start: {
+ line: 3,
+ column: 10
+ },
+ end: {
+ line: 3,
+ column: 12
+ }
+ },
+ \\"1\\": {
+ start: {
+ line: 5,
+ column: 21
+ },
+ end: {
+ line: 7,
+ column: 1
+ }
+ }
+ },
+ fnMap: {},
+ branchMap: {},
+ s: {
+ \\"0\\": 0,
+ \\"1\\": 0
+ },
+ f: {},
+ b: {},
+ _coverageSchema: \\"43e27e138ebf9cfc5966b082cf9a028302ed4184\\",
+ hash: \\"23f56bbaeb2ebcd213f41374d3b2af1bce287bd3\\"
+ };
+ var coverage = global[gcv] || (global[gcv] = {});
+
+ if (coverage[path] && coverage[path].hash === hash) {
+ return coverage[path];
+ }
+
+ return coverage[path] = coverageData;
+}();
+
+import { styled } from '@linaria/react';
+const a = (cov_2dr9r1nt95.s[0]++, 42);
+export const Title = (cov_2dr9r1nt95.s[1]++, /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_tow9xsn\\"
+}));"
+`;
+
+exports[`shaker does not strip instanbul coverage sequences 2`] = `
+
+CSS:
+
+.Title_tow9xsn {
+ height: 42px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker evaluates babel helpers 1`] = `
+"import { styled } from '@linaria/react';
+
+function copyAndExtend(a, b) {
+ return { ...a,
+ ...b
+ };
+}
+
+const obj = copyAndExtend({
+ a: 1
+}, {
+ a: 2
+});
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`shaker evaluates babel helpers 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "2"
+ }
+}
+
+Dependencies: @babel/runtime/helpers/interopRequireDefault, @babel/runtime/helpers/defineProperty
+
+`;
+
+exports[`shaker evaluates complex styles with functions and nested selectors 1`] = `
+"import { css } from '@linaria/core';
+export const bareIconClass = \\"bareIconClass_b1t92lw9\\";
+
+const getSizeStyles = fs => ({
+ [\`&.\${bareIconClass}\`]: {
+ fontSize: fs * 1.5
+ }
+});
+
+export const SIZES = {
+ XS: \\"XS_x1xjmq2i\\"
+};"
+`;
+
+exports[`shaker evaluates complex styles with functions and nested selectors 2`] = `
+
+CSS:
+
+.bareIconClass_b1t92lw9 {}
+.XS_x1xjmq2i {&.bareIconClass_b1t92lw9 { font-size: 16.5px; }}
+
+Dependencies: @babel/runtime/helpers/interopRequireDefault, @babel/runtime/helpers/defineProperty
+
+`;
+
+exports[`shaker evaluates component interpolations 1`] = `
+"const {
+ styled
+} = require('@linaria/react');
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});
+export const Paragraph = /*#__PURE__*/styled(\\"p\\")({
+ name: \\"Paragraph\\",
+ class: \\"Paragraph_p1xjmq2i\\"
+});"
+`;
+
+exports[`shaker evaluates component interpolations 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ color: red;
+}
+.Paragraph_p1xjmq2i {
+ .Title_t1t92lw9 {
+ color: blue;
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker evaluates dependencies with sequence expression 1`] = `
+"import { styled } from '@linaria/react';
+const color = (external, () => 'blue');
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\",
+ vars: {
+ \\"t1t92lw9-0\\": [color]
+ }
+});"
+`;
+
+exports[`shaker evaluates dependencies with sequence expression 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ color: var(--t1t92lw9-0);
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker evaluates expressions with dependencies 1`] = `
+"import { styled } from '@linaria/react';
+import slugify from '@linaria/babel-preset/__fixtures__/slugify';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`shaker evaluates expressions with dependencies 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "6og6jy"
+ }
+}
+
+Dependencies: @babel/runtime/helpers/interopRequireDefault, @linaria/babel-preset/__fixtures__/slugify
+
+`;
+
+exports[`shaker evaluates expressions with expressions depending on shared dependency 1`] = `
+"import { styled } from '@linaria/react';
+
+const slugify = require('@linaria/babel-preset/__fixtures__/slugify').default;
+
+const boo = t => slugify(t) + 'boo';
+
+const bar = t => slugify(t) + 'bar';
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`shaker evaluates expressions with expressions depending on shared dependency 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "6og6jyboo6og6jybar"
+ }
+}
+
+Dependencies: @linaria/babel-preset/__fixtures__/slugify
+
+`;
+
+exports[`shaker evaluates functions with nested identifiers 1`] = `
+"import { styled } from '@linaria/react';
+const objects = {
+ key: {
+ fontSize: 12
+ }
+};
+
+const foo = k => {
+ const obj = objects[k];
+ return obj;
+};
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`shaker evaluates functions with nested identifiers 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ font-size: 12px;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker evaluates identifier in scope 1`] = `
+"import { styled } from '@linaria/react';
+const answer = 42;
+
+const foo = () => answer;
+
+const days = foo() + ' days';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`shaker evaluates identifier in scope 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "42 days"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker evaluates imported typescript enums 1`] = `
+"import { styled } from '@linaria/react';
+import { Colors } from '@linaria/babel-preset/__fixtures__/enums';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_taxxqrn\\"
+});"
+`;
+
+exports[`shaker evaluates imported typescript enums 2`] = `
+
+CSS:
+
+.Title_taxxqrn {
+ color: #27509A;
+}
+
+Dependencies: @linaria/babel-preset/__fixtures__/enums
+
+`;
+
+exports[`shaker evaluates interpolations with sequence expression 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\",
+ vars: {
+ \\"t1t92lw9-0\\": [(external, () => \\"blue\\")]
+ }
+});"
+`;
+
+exports[`shaker evaluates interpolations with sequence expression 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ color: var(--t1t92lw9-0);
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker evaluates local expressions 1`] = `
+"import { styled } from '@linaria/react';
+const answer = 42;
+
+const foo = () => answer;
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`shaker evaluates local expressions 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "42 days"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker evaluates multiple expressions with shared dependency 1`] = `
+"import { styled } from '@linaria/react';
+
+const slugify = require('@linaria/babel-preset/__fixtures__/slugify').default;
+
+const boo = t => slugify(t) + 'boo';
+
+const bar = t => slugify(t) + 'bar';
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`shaker evaluates multiple expressions with shared dependency 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "6og6jyboo"
+ content: "6og6jybar"
+ }
+}
+
+Dependencies: @linaria/babel-preset/__fixtures__/slugify
+
+`;
+
+exports[`shaker evaluates typescript enums 1`] = `
+"import { styled } from '@linaria/react';
+var Colors;
+
+(function (Colors) {
+ Colors[\\"BLUE\\"] = \\"#27509A\\";
+})(Colors || (Colors = {}));
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_taxxqrn\\"
+});"
+`;
+
+exports[`shaker evaluates typescript enums 2`] = `
+
+CSS:
+
+.Title_taxxqrn {
+ color: #27509A;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker generates stable class names 1`] = `
+"import { styled } from '@linaria/react';
+export const T1 = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"T1\\",
+ class: \\"T1_t16d1w5s\\"
+});
+export const T2 = /*#__PURE__*/styled(\\"h2\\")({
+ name: \\"T2\\",
+ class: \\"T2_t18j681n\\"
+});
+export const T3 = /*#__PURE__*/styled(\\"h3\\")({
+ name: \\"T3\\",
+ class: \\"T3_t16ajafo\\"
+});
+export default /*#__PURE__*/styled(\\"p\\")({
+ name: \\"components-library3\\",
+ class: \\"components-library3_c16bmp8w\\"
+});"
+`;
+
+exports[`shaker generates stable class names 2`] = `
+
+CSS:
+
+.T1_t16d1w5s {
+ background: #111;
+}
+.T2_t18j681n {
+ background: #222;
+}
+.T3_t16ajafo {
+ .T2_t18j681n {
+ background: #333;
+ }
+}
+.components-library3_c16bmp8w {
+ background: #333;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker handles complex component 1`] = `
+"// Dead code in this file should be ignored
+import deadDep from 'unknown-dependency';
+import { styled } from '@linaria/react';
+export const deadValue = deadDep();
+const objects = {
+ font: {
+ fontSize: 12
+ },
+ box: {
+ border: '1px solid red'
+ }
+};
+
+const foo = k => {
+ const {
+ [k]: obj
+ } = objects;
+ return obj;
+};
+
+objects.font.fontWeight = 'bold';
+export const whiteColor = '#fff';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t16vg7lb\\"
+});"
+`;
+
+exports[`shaker handles complex component 2`] = `
+
+CSS:
+
+.Title_t16vg7lb {
+ font-size: 12px; font-weight: bold;
+ border: 1px solid red;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker handles escapes properly 1`] = `
+"import { styled } from '@linaria/react';
+const selectors = ['a', 'b'];
+export const Block = /*#__PURE__*/styled(\\"div\\")({
+ name: \\"Block\\",
+ class: \\"Block_b11ngyv0\\"
+});"
+`;
+
+exports[`shaker handles escapes properly 2`] = `
+
+CSS:
+
+.Block_b11ngyv0 {
+ a { content: "\\u000A"; } b { content: "\\u000A"; };
+}
+
+Dependencies: @babel/runtime/helpers/interopRequireDefault, @babel/runtime/helpers/taggedTemplateLiteral
+
+`;
+
+exports[`shaker handles indirect wrapping another styled component 1`] = `
+"const {
+ styled
+} = require('@linaria/react');
+
+const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});
+
+const hoc = Cmp => Cmp;
+
+export const CustomTitle = /*#__PURE__*/styled(hoc(Title))({
+ name: \\"CustomTitle\\",
+ class: \\"CustomTitle_c1xjmq2i\\"
+});"
+`;
+
+exports[`shaker handles indirect wrapping another styled component 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ color: red;
+}
+.CustomTitle_c1xjmq2i {
+ font-size: 24px;
+ color: blue;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker handles wrapping another styled component 1`] = `
+"const {
+ css
+} = require('..');
+
+const {
+ styled
+} = require('@linaria/react');
+
+const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});
+export const BlueTitle = /*#__PURE__*/styled(Title)({
+ name: \\"BlueTitle\\",
+ class: \\"BlueTitle_b1xjmq2i\\"
+});
+export const GreenTitle = /*#__PURE__*/styled(BlueTitle)({
+ name: \\"GreenTitle\\",
+ class: \\"GreenTitle_g2qjq78\\"
+});"
+`;
+
+exports[`shaker handles wrapping another styled component 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ color: red;
+}
+.BlueTitle_b1xjmq2i.Title_t1t92lw9 {
+ font-size: 24px;
+ color: blue;
+}
+.GreenTitle_g2qjq78.BlueTitle_b1xjmq2i.Title_t1t92lw9 {
+ color: green;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker hoistable identifiers 1`] = `
+"import { styled } from '@linaria/react';
+{
+ var days = 42;
+}
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`shaker hoistable identifiers 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "42"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker ignores external expressions 1`] = `
+"import { styled } from '@linaria/react';
+
+const generate = props => props.content;
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\",
+ vars: {
+ \\"t1t92lw9-0\\": [generate]
+ }
+});"
+`;
+
+exports[`shaker ignores external expressions 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "var(--t1t92lw9-0)"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker ignores inline arrow function expressions 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\",
+ vars: {
+ \\"t1t92lw9-0\\": [props => props.content]
+ }
+});"
+`;
+
+exports[`shaker ignores inline arrow function expressions 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "var(--t1t92lw9-0)"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker ignores inline vanilla function expressions 1`] = `
+"import { styled } from '@linaria/react';
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\",
+ vars: {
+ \\"t1t92lw9-0\\": [function (props) {
+ return props.content;
+ }]
+ }
+});"
+`;
+
+exports[`shaker ignores inline vanilla function expressions 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ &:before {
+ content: "var(--t1t92lw9-0)"
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker inlines array styles as CSS string 1`] = `
+"import { styled } from '@linaria/react';
+
+const fill = (top = 0, left = 0, right = 0, bottom = 0) => [{
+ position: 'absolute'
+}, {
+ top,
+ right,
+ bottom,
+ left
+}];
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`shaker inlines array styles as CSS string 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ position: absolute; top: 0; right: 0; bottom: 0; left: 0;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker inlines object styles as CSS string 1`] = `
+"import { styled } from '@linaria/react';
+
+const fill = (top = 0, left = 0, right = 0, bottom = 0) => ({
+ position: 'absolute',
+ top,
+ right,
+ bottom,
+ left
+});
+
+export const Title = /*#__PURE__*/styled(\\"h1\\")({
+ name: \\"Title\\",
+ class: \\"Title_t1t92lw9\\"
+});"
+`;
+
+exports[`shaker inlines object styles as CSS string 2`] = `
+
+CSS:
+
+.Title_t1t92lw9 {
+ position: absolute; top: 0; right: 0; bottom: 0; left: 0;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker it should not throw location error for hoisted identifier 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+
+const size = () => 5;
+
+var _ref = size();
+
+export default function Component() {
+ const color = _ref;
+ return \\"source0_s1t92lw9\\";
+}"
+`;
+
+exports[`shaker it should not throw location error for hoisted identifier 2`] = `
+
+CSS:
+
+.source0_s1t92lw9 {opacity:5;}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker non-hoistable identifiers 1`] = `
+"<<DIRNAME>>/source.js: An error occurred when evaluating the expression:
+
+ > days is not defined.
+
+ Make sure you are not using a browser or Node specific API and all the variables are available in static context.
+ Linaria have to extract pieces of your code to resolve the interpolated values.
+ Defining styled component or class will not work inside:
+ - function,
+ - class,
+ - method,
+ - loop,
+ because it cannot be statically determined in which context you use them.
+ That's why some variables may be not defined during evaluation.
+
+ 7 | export const Title = styled.h1\`
+ 8 | &:before {
+> 9 | content: \\"\${days}\\"
+ | ^^^^
+ 10 | }
+ 11 | \`;"
+`;
+
+exports[`shaker should handle shadowed identifier inside components 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+const color = 'red';
+var _ref = 'blue';
+var _ref2 = {
+ color: _ref
+};
+export default function Component() {
+ const color = _ref;
+ const val = _ref2;
+ return React.createElement('div', {
+ className: \\"className_c1t92lw9\\"
+ });
+}"
+`;
+
+exports[`shaker should handle shadowed identifier inside components 2`] = `
+
+CSS:
+
+.className_c1t92lw9 {background-color:blue;}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker should interpolate imported components 1`] = `
+"import { css } from \\"@linaria/core\\";
+import { Title } from \\"@linaria/babel-preset/__fixtures__/complex-component\\";
+export const square = \\"square_s1t92lw9\\";"
+`;
+
+exports[`shaker should interpolate imported components 2`] = `
+
+CSS:
+
+.square_s1t92lw9 {
+ .Title_t16vg7lb {
+ color: red;
+ }
+}
+
+Dependencies: @linaria/babel-preset/__fixtures__/complex-component
+
+`;
+
+exports[`shaker should interpolate imported variables 1`] = `
+"import { css } from \\"@linaria/core\\";
+import { whiteColor } from \\"@linaria/babel-preset/__fixtures__/complex-component\\";
+export const square = \\"square_s1t92lw9\\";"
+`;
+
+exports[`shaker should interpolate imported variables 2`] = `
+
+CSS:
+
+.square_s1t92lw9 {
+ color: #fff
+}
+
+Dependencies: @linaria/babel-preset/__fixtures__/complex-component
+
+`;
+
+exports[`shaker should process \`css\` calls inside components 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+export function Component() {
+ const opacity = 0.2;
+ const className = \\"className_c1t92lw9\\";
+ return React.createElement(\\"div\\", {
+ className
+ });
+}"
+`;
+
+exports[`shaker should process \`css\` calls inside components 2`] = `
+
+CSS:
+
+.className_c1t92lw9 {
+ opacity: 0.2;
+ }
+
+Dependencies: NA
+
+`;
+
+exports[`shaker should process \`css\` calls with complex interpolation inside components 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+import externalDep from '@linaria/babel-preset/__fixtures__/sample-script';
+const globalObj = {
+ opacity: 0.5
+};
+var _ref = externalDep;
+var _ref2 = {
+ value: 0.2,
+ cell: \\"cell_c1t92lw9\\"
+};
+var _ref3 = _ref2;
+export function Component() {
+ const classes = _ref2;
+ const classes2 = _ref3;
+ const referencedExternalDep = _ref;
+ const className = \\"className_c1xjmq2i\\";
+ return React.createElement(\\"div\\", {
+ className
+ });
+}"
+`;
+
+exports[`shaker should process \`css\` calls with complex interpolation inside components 2`] = `
+
+CSS:
+
+.cell_c1t92lw9 {
+ opacity: 0;
+ }
+.className_c1xjmq2i {
+ opacity: 0.5;
+ font-size: 42
+ font-size: 42
+
+ &:hover .cell_c1t92lw9 {
+ opacity: 0.2;
+ }
+ }
+
+Dependencies: @babel/runtime/helpers/interopRequireDefault, @linaria/babel-preset/__fixtures__/sample-script
+
+`;
+
+exports[`shaker should process \`styled\` calls inside components 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+export function Component() {
+ const opacity = 0.2;
+ const MyComponent = styled.h1\`
+ opacity: \${opacity};
+ \`;
+ return React.createElement(MyComponent);
+}"
+`;
+
+exports[`shaker should process \`styled\` calls inside components 2`] = `Object {}`;
+
+exports[`shaker should process \`styled\` calls with complex interpolation inside components 1`] = `
+"import React from 'react';
+import { css } from '@linaria/core';
+const globalObj = {
+ opacity: 0.5
+};
+const Styled1 = styled.p\`
+ opacity: \${globalObj.opacity}
+\`;
+export function Component() {
+ const classes = {
+ value: 0.2,
+ cell: \\"cell_c1t92lw9\\"
+ };
+ const classes2 = classes;
+ const MyComponent = styled\`
+ opacity: \${globalObj.opacity};
+
+ &:hover .\${classes2.cell} {
+ opacity: \${classes.value};
+ }
+ \${Styled1} {
+ font-size: 1;
+ }
+ \`;
+ return React.createElement(MyComponent);
+}"
+`;
+
+exports[`shaker should process \`styled\` calls with complex interpolation inside components 2`] = `
+
+CSS:
+
+.cell_c1t92lw9 {
+ opacity: 0;
+ }
+
+Dependencies: NA
+
+`;
+
+exports[`shaker should work with String and Number object 1`] = `
+"import { css } from '@linaria/core';
+export const style = \\"style_s1t92lw9\\";"
+`;
+
+exports[`shaker should work with String and Number object 2`] = `
+
+CSS:
+
+.style_s1t92lw9 {
+ width: 100%;
+ opacity: 0.75;
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker should work with generated classnames as selectors 1`] = `
+"import { css } from \\"@linaria/core\\";
+export const text = \\"text_t1t92lw9\\";
+export const square = \\"square_s1xjmq2i\\";"
+`;
+
+exports[`shaker should work with generated classnames as selectors 2`] = `
+
+CSS:
+
+.text_t1t92lw9 {}
+.square_s1xjmq2i {
+ .text_t1t92lw9 {
+ color: red;
+ }
+}
+
+Dependencies: NA
+
+`;
+
+exports[`shaker should work with wildcard imports 1`] = `
+"import { css } from \\"@linaria/core\\";
+import * as mod from \\"@linaria/babel-preset/__fixtures__/complex-component\\";
+const color = mod[\\"whiteColor\\"];
+export const square = \\"square_s1t92lw9\\";"
+`;
+
+exports[`shaker should work with wildcard imports 2`] = `
+
+CSS:
+
+.square_s1t92lw9 {
+ .Title_t16vg7lb {
+ color: #fff;
+ }
+}
+
+Dependencies: @babel/runtime/helpers/typeof, @linaria/babel-preset/__fixtures__/complex-component
+
+`;
+
+exports[`shaker should work with wildcard reexports 1`] = `
+"import { css } from \\"@linaria/core\\";
+import { foo1 } from \\"../__fixtures__/reexports\\";
+export const square = \\"square_s1t92lw9\\";"
+`;
+
+exports[`shaker should work with wildcard reexports 2`] = `
+
+CSS:
+
+.square_s1t92lw9 {
+ color: foo1;
+}
+
+Dependencies: ../__fixtures__/reexports
+
+`;
+
+exports[`shaker simplifies react components 1`] = `
+"import React from 'react';
+import { styled } from '@linaria/react';
+import constant from './broken-dependency';
+
+const FuncComponent = props => <div>{props.children + constant}</div>;
+
+class ClassComponent extends React.PureComponent {
+ method() {
+ return constant;
+ }
+
+ render() {
+ return <div>{props.children + constant}</div>;
+ }
+
+}
+
+export const StyledFunc = /*#__PURE__*/styled(FuncComponent)({
+ name: \\"StyledFunc\\",
+ class: \\"StyledFunc_s1t92lw9\\"
+});
+export const StyledClass = /*#__PURE__*/styled(ClassComponent)({
+ name: \\"StyledClass\\",
+ class: \\"StyledClass_s1xjmq2i\\"
+});"
+`;
+
+exports[`shaker simplifies react components 2`] = `
+
+CSS:
+
+.StyledFunc_s1t92lw9 {
+ color: red;
+}
+.StyledClass_s1xjmq2i {
+ color: blue;
+}
+
+Dependencies: @babel/runtime/helpers/interopRequireDefault, react
+
+`;
+
+exports[`shaker throws codeframe error when evaluation fails 1`] = `
+"<<DIRNAME>>/source.js: An error occurred when evaluating the expression:
+
+ > This will fail.
+
+ Make sure you are not using a browser or Node specific API and all the variables are available in static context.
+ Linaria have to extract pieces of your code to resolve the interpolated values.
+ Defining styled component or class will not work inside:
+ - function,
+ - class,
+ - method,
+ - loop,
+ because it cannot be statically determined in which context you use them.
+ That's why some variables may be not defined during evaluation.
+
+ 4 |
+ 5 | export const Title = styled.h1\`
+> 6 | font-size: \${foo()}px;
+ | ^^^^^
+ 7 | \`;"
+`;
+
+exports[`shaker throws if couldn't determine a display name 1`] = `
+"<<DIRNAME>>/.js: Couldn't determine a name for the component. Ensure that it's either:
+- Assigned to a variable
+- Is an object property
+- Is a prop in a JSX element
+
+ 1 | import { styled } from '@linaria/react';
+ 2 |
+> 3 | export default styled.h1\`
+ | ^
+ 4 | font-size: 14px;
+ 5 | \`;"
+`;
+
+exports[`shaker throws when interpolation evaluates to NaN 1`] = `
+"<<DIRNAME>>/source.js: The expression evaluated to 'NaN', which is probably a mistake. If you want it to be inserted into CSS, explicitly cast or transform the value to a string, e.g. - 'String(height)'.
+ 4 |
+ 5 | export const Title = styled.h1\`
+> 6 | height: \${height}px;
+ | ^^^^^^
+ 7 | \`;"
+`;
+
+exports[`shaker throws when interpolation evaluates to null 1`] = `
+"<<DIRNAME>>/source.js: The expression evaluated to 'null', which is probably a mistake. If you want it to be inserted into CSS, explicitly cast or transform the value to a string, e.g. - 'String(color)'.
+ 4 |
+ 5 | export const Title = styled.h1\`
+> 6 | color: \${color};
+ | ^^^^^
+ 7 | \`;"
+`;
+
+exports[`shaker throws when interpolation evaluates to undefined 1`] = `
+"<<DIRNAME>>/source.js: The expression evaluated to 'undefined', which is probably a mistake. If you want it to be inserted into CSS, explicitly cast or transform the value to a string, e.g. - 'String(fontSize)'.
+ 4 |
+ 5 | export const Title = styled.h1\`
+> 6 | font-size: \${fontSize};
+ | ^^^^^^^^
+ 7 | \`;"
+`;
diff --git a/@linaria/packages/shaker/__tests__/shaker.test.ts b/@linaria/packages/shaker/__tests__/shaker.test.ts
new file mode 100644
index 0000000..516670d
--- /dev/null
+++ b/@linaria/packages/shaker/__tests__/shaker.test.ts
@@ -0,0 +1,100 @@
+import { run } from '@linaria/babel-preset/__utils__/strategy-tester';
+import dedent from 'dedent';
+
+describe('shaker', () => {
+ run(__dirname, require('../src').default, (transpile) => {
+ it('should work with wildcard imports', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from "@linaria/core";
+ import * as mod from "@linaria/babel-preset/__fixtures__/complex-component";
+
+ const color = mod["whiteColor"];
+
+ export const square = css\`
+ ${'${mod.Title}'} {
+ color: ${'${color}'};
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('should work with wildcard reexports', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from "@linaria/core";
+ import { foo1 } from "../__fixtures__/reexports";
+
+ export const square = css\`
+ color: ${'${foo1}'};
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('should interpolate imported components', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from "@linaria/core";
+ import { Title } from "@linaria/babel-preset/__fixtures__/complex-component";
+
+ export const square = css\`
+ ${'${Title}'} {
+ color: red;
+ }
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('should interpolate imported variables', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { css } from "@linaria/core";
+ import { whiteColor } from "@linaria/babel-preset/__fixtures__/complex-component";
+
+ export const square = css\`
+ color: ${'${whiteColor}'}
+ \`;
+ `
+ );
+
+ expect(code).toMatchSnapshot();
+ expect(metadata).toMatchSnapshot();
+ });
+
+ it('evaluates typescript enums', async () => {
+ const { code, metadata } = await transpile(
+ dedent`
+ import { styled } from '@linaria/react';
+
+ enum Colors {
+ BLUE = '#27509A'
+ }
+
+ 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();
+ });
+ });
+});
diff --git a/@linaria/packages/shaker/babel.config.js b/@linaria/packages/shaker/babel.config.js
new file mode 100644
index 0000000..c9ad680
--- /dev/null
+++ b/@linaria/packages/shaker/babel.config.js
@@ -0,0 +1,3 @@
+const config = require('../../babel.config');
+
+module.exports = config;
diff --git a/@linaria/packages/shaker/package.json b/@linaria/packages/shaker/package.json
new file mode 100644
index 0000000..5c12659
--- /dev/null
+++ b/@linaria/packages/shaker/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "@linaria/shaker",
+ "version": "3.0.0-beta.11",
+ "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/dedent": "^0.7.0",
+ "dedent": "^0.7.0"
+ },
+ "dependencies": {
+ "@babel/generator": ">=7",
+ "@babel/plugin-transform-runtime": ">=7",
+ "@babel/plugin-transform-template-literals": ">=7",
+ "@babel/preset-env": ">=7",
+ "@linaria/babel-preset": "^3.0.0-beta.7",
+ "@linaria/logger": "^3.0.0-beta.3",
+ "@linaria/preeval": "^3.0.0-beta.8",
+ "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
+ "ts-invariant": "^0.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": ">=7"
+ }
+}
diff --git a/@linaria/packages/shaker/src/DepsGraph.ts b/@linaria/packages/shaker/src/DepsGraph.ts
new file mode 100644
index 0000000..45fbe21
--- /dev/null
+++ b/@linaria/packages/shaker/src/DepsGraph.ts
@@ -0,0 +1,149 @@
+import { types as t } from '@babel/core';
+import ScopeManager, { PromisedNode, resolveNode } from './scope';
+
+type Action = (this: DepsGraph, a: t.Node, b: t.Node) => void;
+
+function addEdge(this: DepsGraph, a: t.Node, b: t.Node) {
+ if (this.dependencies.has(a) && this.dependencies.get(a)!.has(b)) {
+ // edge has been already added∂ƒ
+ return;
+ }
+
+ this.edges.push([a, b]);
+ if (this.dependencies.has(a)) {
+ this.dependencies.get(a)!.add(b);
+ } else {
+ this.dependencies.set(a, new Set([b]));
+ }
+
+ if (this.dependents.has(b)) {
+ this.dependents.get(b)!.add(a);
+ } else {
+ this.dependents.set(b, new Set([a]));
+ }
+}
+
+export default class DepsGraph {
+ public readonly imports: Map<string, (t.Identifier | t.StringLiteral)[]> =
+ new Map();
+ public readonly importAliases: Map<t.Identifier, string> = new Map();
+ public readonly importTypes: Map<
+ string,
+ 'wildcard' | 'default' | 'reexport'
+ > = new Map();
+ public readonly reexports: Array<t.Identifier> = [];
+
+ protected readonly parents: WeakMap<t.Node, t.Node> = new WeakMap();
+ protected readonly edges: Array<[t.Node, t.Node]> = [];
+ protected readonly exports: Map<string, t.Node> = new Map();
+ protected readonly dependencies: Map<t.Node, Set<t.Node>> = new Map();
+ protected readonly dependents: Map<t.Node, Set<t.Node>> = new Map();
+
+ private actionQueue: Array<
+ [Action, t.Node | PromisedNode, t.Node | PromisedNode]
+ > = [];
+
+ private processQueue() {
+ if (this.actionQueue.length === 0) {
+ return;
+ }
+
+ for (const [action, a, b] of this.actionQueue) {
+ const resolvedA = resolveNode(a);
+ const resolvedB = resolveNode(b);
+ if (resolvedA && resolvedB) {
+ action.call(this, resolvedA, resolvedB);
+ }
+ }
+
+ this.actionQueue = [];
+ }
+
+ private getAllReferences(id: string): (t.Identifier | t.MemberExpression)[] {
+ const [, name] = id.split(':');
+ const declaration = this.scope.getDeclaration(id)!;
+ const allReferences: (t.Identifier | t.MemberExpression)[] = [
+ ...Array.from(this.dependencies.get(declaration) || []),
+ ...Array.from(this.dependents.get(declaration) || []),
+ ].filter((i) => t.isIdentifier(i) && i.name === name) as t.Identifier[];
+ allReferences.push(declaration);
+ return allReferences;
+ }
+
+ constructor(protected scope: ScopeManager) {}
+
+ addEdge(dependent: t.Node | PromisedNode, dependency: t.Node | PromisedNode) {
+ this.actionQueue.push([addEdge, dependent, dependency]);
+ }
+
+ addExport(name: string, node: t.Node) {
+ this.exports.set(name, node);
+ }
+
+ addParent(node: t.Node, parent: t.Node) {
+ this.parents.set(node, parent);
+ }
+
+ getParent(node: t.Node): t.Node | undefined {
+ return this.parents.get(node);
+ }
+
+ getDependenciesByBinding(id: string) {
+ this.processQueue();
+ const allReferences = this.getAllReferences(id);
+ const dependencies = [];
+ for (let [a, b] of this.edges) {
+ if (t.isIdentifier(a) && allReferences.includes(a)) {
+ dependencies.push(b);
+ }
+ }
+
+ return dependencies;
+ }
+
+ getDependentsByBinding(id: string) {
+ this.processQueue();
+ const allReferences = this.getAllReferences(id);
+ const dependents = [];
+ for (let [a, b] of this.edges) {
+ if (t.isIdentifier(b) && allReferences.includes(b)) {
+ dependents.push(a);
+ }
+ }
+
+ return dependents;
+ }
+
+ findDependencies(like: Object) {
+ this.processQueue();
+ return this.edges
+ .filter(([a]) => t.shallowEqual(a, like))
+ .map(([, b]) => b);
+ }
+
+ findDependents(like: object) {
+ this.processQueue();
+ return this.edges
+ .filter(([, b]) => t.shallowEqual(b, like))
+ .map(([a]) => a);
+ }
+
+ getDependencies(nodes: t.Node[]) {
+ this.processQueue();
+ return nodes.reduce(
+ (acc, node) => acc.concat(Array.from(this.dependencies.get(node) || [])),
+ [] as t.Node[]
+ );
+ }
+
+ getLeaf(name: string): t.Node | undefined {
+ return this.exports.get(name);
+ }
+
+ getLeaves(only: string[] | null): Array<t.Node | undefined> {
+ this.processQueue();
+ return only
+ ? only.map((name) => this.getLeaf(name))
+ : Array.from(this.exports.values());
+ }
+}
diff --git a/@linaria/packages/shaker/src/GraphBuilderState.ts b/@linaria/packages/shaker/src/GraphBuilderState.ts
new file mode 100644
index 0000000..ba70ca2
--- /dev/null
+++ b/@linaria/packages/shaker/src/GraphBuilderState.ts
@@ -0,0 +1,45 @@
+import type { Node, VisitorKeys } from '@babel/types';
+import ScopeManager from './scope';
+import DepsGraph from './DepsGraph';
+
+export type OnVisitCallback = (n: Node) => void;
+
+export default abstract class GraphBuilderState {
+ public readonly scope = new ScopeManager();
+ public readonly graph = new DepsGraph(this.scope);
+ public readonly meta = new Map<string, any>();
+
+ protected callbacks: OnVisitCallback[] = [];
+
+ /*
+ * For expressions like `{ foo: bar }` we need to now context
+ *
+ * const obj = { foo: bar };
+ * Here context is `expression`, `bar` is a variable which depends from its declaration.
+ *
+ * const { foo: bar } = obj;
+ * Here context is `pattern` and `bar` is a variable declaration itself.
+ */
+ public readonly context: Array<'expression' | 'lval'> = [];
+
+ public readonly fnStack: Node[] = [];
+
+ public onVisit(callback: OnVisitCallback) {
+ this.callbacks.push(callback);
+ return () => {
+ this.callbacks = this.callbacks.filter((c) => c !== callback);
+ };
+ }
+
+ abstract baseVisit<TNode extends Node>(
+ node: TNode,
+ ignoreDeps?: boolean
+ ): void;
+
+ abstract visit<TNode extends Node, TParent extends Node>(
+ node: TNode,
+ parent: TParent | null,
+ parentKey: VisitorKeys[TParent['type']] | null,
+ listIdx?: number | null
+ ): void;
+}
diff --git a/@linaria/packages/shaker/src/Visitors.ts b/@linaria/packages/shaker/src/Visitors.ts
new file mode 100644
index 0000000..0228d4a
--- /dev/null
+++ b/@linaria/packages/shaker/src/Visitors.ts
@@ -0,0 +1,87 @@
+import { types as t } from '@babel/core';
+import type { Identifier, Node, VisitorKeys } from '@babel/types';
+import { warn } from '@linaria/logger';
+import { peek } from '@linaria/babel-preset';
+import GraphBuilderState from './GraphBuilderState';
+import identifierHandlers from './identifierHandlers';
+import type { Visitor, Visitors } from './types';
+
+import { visitors as core } from './langs/core';
+
+const visitors: Visitors = {
+ Identifier<TParent extends Node>(
+ this: GraphBuilderState,
+ node: Identifier,
+ parent: TParent | null,
+ parentKey: VisitorKeys[TParent['type']] | null,
+ listIdx: number | null = null
+ ) {
+ if (!parent || !parentKey) {
+ return;
+ }
+
+ const handler = identifierHandlers[`${parent.type}:${parentKey}`];
+
+ if (typeof handler === 'function') {
+ handler(this, node, parent, parentKey, listIdx);
+ return;
+ }
+
+ if (handler === 'keep') {
+ return;
+ }
+
+ if (handler === 'declare') {
+ const kindOfDeclaration = this.meta.get('kind-of-declaration');
+ this.scope.declare(node, kindOfDeclaration === 'var', null);
+ return;
+ }
+
+ if (handler === 'refer') {
+ const declaration = this.scope.addReference(node);
+ // Let's check that it's not a global variable
+ if (declaration) {
+ // usage of a variable depends on its declaration
+ this.graph.addEdge(node, declaration);
+
+ const context = peek(this.context);
+ if (context === 'lval') {
+ // This is an identifier in the left side of an assignment expression and a variable value depends on that.
+ this.graph.addEdge(declaration, node);
+ }
+ }
+
+ return;
+ }
+
+ /*
+ * There is an unhandled identifier.
+ * This case should be added to ./identifierHandlers.ts
+ */
+ warn(
+ 'evaluator:shaker',
+ 'Unhandled identifier',
+ node.name,
+ parent.type,
+ parentKey,
+ listIdx
+ );
+ },
+
+ ...core,
+};
+
+const isKeyOfVisitors = (type: string): type is keyof Visitors =>
+ type in visitors;
+
+export function getVisitors<TNode extends Node>(node: TNode): Visitor<TNode>[] {
+ const aliases = t.ALIAS_KEYS[node.type] || [];
+ const aliasVisitors = aliases
+ .map((type) => (isKeyOfVisitors(type) ? visitors[type] : null))
+ .filter((i) => i) as Visitor<TNode>[];
+ return [...aliasVisitors, visitors[node.type] as Visitor<TNode>].filter(
+ (v) => v
+ );
+}
+
+export default visitors;
diff --git a/@linaria/packages/shaker/src/dumpNode.ts b/@linaria/packages/shaker/src/dumpNode.ts
new file mode 100644
index 0000000..f6d1c84
--- /dev/null
+++ b/@linaria/packages/shaker/src/dumpNode.ts
@@ -0,0 +1,63 @@
+import { types as t } from '@babel/core';
+import type {
+ BinaryExpression,
+ Identifier,
+ Node,
+ NumericLiteral,
+ StringLiteral,
+} from '@babel/types';
+
+type Hooks = {
+ [key: string]: (node: any) => string | number;
+};
+
+const hooks: Hooks = {
+ Identifier: (node: Identifier) => node.name,
+ BinaryExpression: (node: BinaryExpression) => node.operator,
+ NumericLiteral: (node: NumericLiteral) => node.value,
+ StringLiteral: (node: StringLiteral) => node.value,
+};
+
+function isNode(obj: any): obj is Node {
+ return !!obj;
+}
+
+export default function dumpNode<T extends Node>(
+ node: T,
+ alive: Set<Node> | null = null,
+ level = 0,
+ idx: number | null = null
+) {
+ let result = level === 0 ? '\n' : '';
+ const prefix =
+ level === 0
+ ? ''
+ : `${'| '.repeat(level - 1)}${idx === null ? '|' : idx}${
+ (idx || 0) < 10 ? '=' : ''
+ }`;
+
+ const { type } = node;
+ result += `${prefix}${type}${type in hooks ? ` ${hooks[type](node)}` : ''}`;
+
+ if (alive) {
+ result += alive.has(node) ? ' ✅' : ' ❌';
+ }
+
+ result += '\n';
+ const keys = t.VISITOR_KEYS[type] as Array<keyof T>;
+ for (const key of keys) {
+ const subNode = node[key];
+
+ result += `${'| '.repeat(level)}|-${key}\n`;
+ if (Array.isArray(subNode)) {
+ for (let i = 0; i < subNode.length; i++) {
+ const child = subNode[i];
+ if (child) result += dumpNode(child, alive, level + 2, i);
+ }
+ } else if (isNode(subNode)) {
+ result += dumpNode(subNode, alive, level + 2);
+ }
+ }
+
+ return result;
+}
diff --git a/@linaria/packages/shaker/src/graphBuilder.ts b/@linaria/packages/shaker/src/graphBuilder.ts
new file mode 100644
index 0000000..6b167e8
--- /dev/null
+++ b/@linaria/packages/shaker/src/graphBuilder.ts
@@ -0,0 +1,192 @@
+import { types as t } from '@babel/core';
+import type { AssignmentExpression, Node, VisitorKeys } from '@babel/types';
+import { isNode, getVisitorKeys } from '@linaria/babel-preset';
+import DepsGraph from './DepsGraph';
+import GraphBuilderState from './GraphBuilderState';
+import { getVisitors } from './Visitors';
+import type { VisitorAction } from './types';
+import ScopeManager from './scope';
+
+const isVoid = (node: Node): boolean =>
+ t.isUnaryExpression(node) && node.operator === 'void';
+
+class GraphBuilder extends GraphBuilderState {
+ static build(root: Node): DepsGraph {
+ return new GraphBuilder(root).graph;
+ }
+
+ constructor(rootNode: Node) {
+ super();
+
+ this.visit(rootNode, null, null, null);
+ }
+
+ private isExportsIdentifier(node: Node) {
+ if (
+ t.isIdentifier(node) &&
+ this.scope.getDeclaration(node) === ScopeManager.globalExportsIdentifier
+ ) {
+ return true;
+ }
+
+ return (
+ t.isMemberExpression(node) &&
+ t.isIdentifier(node.property) &&
+ node.property.name === 'exports' &&
+ t.isIdentifier(node.object) &&
+ this.scope.getDeclaration(node.object) ===
+ ScopeManager.globalModuleIdentifier
+ );
+ }
+
+ private isExportsAssigment(node: Node): node is AssignmentExpression {
+ if (
+ node &&
+ t.isAssignmentExpression(node) &&
+ t.isMemberExpression(node.left)
+ ) {
+ if (this.isExportsIdentifier(node.left)) {
+ // This is a default export like `module.exports = 42`
+ return true;
+ }
+
+ if (this.isExportsIdentifier(node.left.object)) {
+ // This is a named export like `module.exports.a = 42` or `exports.a = 42`
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /*
+ * Implements a default behaviour for AST-nodes:
+ * • visits every child;
+ * • if the current node is an Expression node, adds all its children as dependencies.
+ *
+ * eg. BinaryExpression has children `left` and `right`,
+ * both of them are required for evaluating the value of the expression
+ */
+ baseVisit<TNode extends Node>(node: TNode, ignoreDeps = false) {
+ const dependencies = [];
+ const isExpression = t.isExpression(node);
+ const keys = getVisitorKeys(node);
+ for (const key of keys) {
+ // Ignore all types
+ if (key === 'typeArguments' || key === 'typeParameters') {
+ continue;
+ }
+
+ const subNode = node[key as keyof TNode];
+
+ if (Array.isArray(subNode)) {
+ for (let i = 0; i < subNode.length; i++) {
+ const child = subNode[i];
+ if (child && this.visit(child, node, key, i) !== 'ignore') {
+ dependencies.push(child);
+ }
+ }
+ } else if (
+ isNode(subNode) &&
+ this.visit(subNode, node, key) !== 'ignore'
+ ) {
+ dependencies.push(subNode);
+ }
+ }
+
+ if (isExpression && !ignoreDeps) {
+ dependencies.forEach((dep) => this.graph.addEdge(node, dep));
+ }
+
+ this.callbacks.forEach((callback) => callback(node));
+ }
+
+ visit<TNode extends Node, TParent extends Node>(
+ node: TNode,
+ parent: TParent | null,
+ parentKey: VisitorKeys[TParent['type']] | null,
+ listIdx: number | null = null
+ ): VisitorAction {
+ if (parent) {
+ this.graph.addParent(node, parent);
+ }
+
+ if (
+ this.isExportsAssigment(node) &&
+ !this.isExportsAssigment(node.right) &&
+ !isVoid(node.right)
+ ) {
+ if (
+ t.isMemberExpression(node.left) &&
+ (t.isIdentifier(node.left.property) ||
+ t.isStringLiteral(node.left.property))
+ ) {
+ if (
+ t.isIdentifier(node.left.object) &&
+ node.left.object.name === 'module'
+ ) {
+ // It's a batch or default export
+ if (t.isObjectExpression(node.right)) {
+ // Batch export is a very particular case.
+ // Each property of the assigned object is independent named export.
+ // We also need to specify all dependencies and call `visit` for every value.
+ this.visit(node.left, node, 'left');
+ node.right.properties.forEach((prop) => {
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
+ this.visit(prop.value, prop, 'value');
+ this.graph.addExport(prop.key.name, prop);
+ this.graph.addEdge(prop, node.right);
+ this.graph.addEdge(prop, prop.key);
+ this.graph.addEdge(prop.key, prop.value);
+ }
+ });
+
+ this.graph.addEdge(node.right, node);
+ this.graph.addEdge(node, node.left);
+
+ // We have done all the required work, so stop here
+ return;
+ } else {
+ this.graph.addExport('default', node);
+ }
+ } else {
+ // it can be either `exports.name` or `exports["name"]`
+ const nameNode = node.left.property;
+ this.graph.addExport(
+ t.isStringLiteral(nameNode) ? nameNode.value : nameNode.name,
+ node
+ );
+ }
+ }
+ }
+
+ const isScopable = t.isScopable(node);
+ const isFunction = t.isFunction(node);
+
+ if (isScopable) this.scope.new(t.isProgram(node) || t.isFunction(node));
+ if (isFunction) this.fnStack.push(node);
+
+ const visitors = getVisitors(node);
+ let action: VisitorAction;
+ if (visitors.length > 0) {
+ let visitor;
+ while (!action && (visitor = visitors.shift())) {
+ action = visitor.call(this, node, parent, parentKey, listIdx);
+ }
+ } else {
+ this.baseVisit(node);
+ }
+
+ if (parent && action !== 'ignore') {
+ // Node always depends on its parent
+ this.graph.addEdge(node, parent);
+ }
+
+ if (isFunction) this.fnStack.pop();
+ if (isScopable) this.scope.dispose();
+
+ return action;
+ }
+}
+
+export default GraphBuilder.build;
diff --git a/@linaria/packages/shaker/src/identifierHandlers.ts b/@linaria/packages/shaker/src/identifierHandlers.ts
new file mode 100644
index 0000000..bd3c919
--- /dev/null
+++ b/@linaria/packages/shaker/src/identifierHandlers.ts
@@ -0,0 +1,130 @@
+import { types as t } from '@babel/core';
+import type { Aliases, Identifier, Node, VisitorKeys } from '@babel/types';
+import { peek } from '@linaria/babel-preset';
+import GraphBuilderState from './GraphBuilderState';
+import type { IdentifierHandlerType, NodeType } from './types';
+import { identifierHandlers as core } from './langs/core';
+import ScopeManager from './scope';
+
+type HandlerFn = <TParent extends Node = Node>(
+ builder: GraphBuilderState,
+ node: Identifier,
+ parent: TParent,
+ parentKey: VisitorKeys[TParent['type']],
+ listIdx: number | null
+) => void;
+
+type Handler = IdentifierHandlerType | HandlerFn;
+
+const handlers: {
+ [key: string]: Handler;
+} = {};
+
+function isAlias(type: NodeType): type is keyof Aliases {
+ return type in t.FLIPPED_ALIAS_KEYS;
+}
+
+export function defineHandler(
+ typeOrAlias: NodeType,
+ field: string,
+ handler: Handler
+) {
+ const types = isAlias(typeOrAlias)
+ ? t.FLIPPED_ALIAS_KEYS[typeOrAlias]
+ : [typeOrAlias];
+ types.forEach((type: string) => {
+ handlers[`${type}:${field}`] = handler;
+ });
+}
+
+export function batchDefineHandlers(
+ typesAndFields: [NodeType, ...string[]][],
+ handler: IdentifierHandlerType
+) {
+ typesAndFields.forEach(([type, ...fields]) =>
+ fields.forEach((field) => defineHandler(type, field, handler))
+ );
+}
+
+batchDefineHandlers([...core.declare], 'declare');
+
+batchDefineHandlers([...core.keep], 'keep');
+
+batchDefineHandlers([...core.refer], 'refer');
+
+/*
+ * Special case for FunctionDeclaration
+ * Function id should be defined in the parent scope
+ */
+defineHandler(
+ 'FunctionDeclaration',
+ 'id',
+ (builder: GraphBuilderState, node: Identifier) => {
+ builder.scope.declare(node, false, null, 1);
+ }
+);
+
+/*
+ * Special handler for [obj.member = 42] = [1] in different contexts
+ */
+const memberExpressionObjectHandler = (
+ builder: GraphBuilderState,
+ node: Identifier
+) => {
+ const context = peek(builder.context);
+ const declaration = builder.scope.addReference(node);
+ if (declaration) {
+ builder.graph.addEdge(node, declaration);
+
+ if (context === 'lval') {
+ // One exception here: we shake exports,
+ // so `exports` does not depend on its members' assignments.
+ if (
+ declaration !== ScopeManager.globalExportsIdentifier &&
+ declaration !== ScopeManager.globalModuleIdentifier
+ ) {
+ builder.graph.addEdge(declaration, node);
+ }
+ }
+ }
+};
+
+defineHandler('MemberExpression', 'object', memberExpressionObjectHandler);
+defineHandler(
+ 'OptionalMemberExpression',
+ 'object',
+ memberExpressionObjectHandler
+);
+
+/*
+ * Special handler for obj.member and obj[member]
+ */
+const memberExpressionPropertyHandler = (
+ builder: GraphBuilderState,
+ node: Identifier,
+ parent: Node
+) => {
+ if (t.isMemberExpression(parent) && parent.computed) {
+ const declaration = builder.scope.addReference(node);
+ // Let's check that it's not a global variable
+ if (declaration) {
+ // usage of a variable depends on its declaration
+ builder.graph.addEdge(node, declaration);
+
+ const context = peek(builder.context);
+ if (context === 'lval') {
+ // This is an identifier in the left side of an assignment expression and a variable value depends on that.
+ builder.graph.addEdge(declaration, node);
+ }
+ }
+ }
+};
+
+defineHandler('MemberExpression', 'property', memberExpressionPropertyHandler);
+defineHandler(
+ 'OptionalMemberExpression',
+ 'property',
+ memberExpressionPropertyHandler
+);
+
+export default handlers;
diff --git a/@linaria/packages/shaker/src/index.ts b/@linaria/packages/shaker/src/index.ts
new file mode 100644
index 0000000..f71353b
--- /dev/null
+++ b/@linaria/packages/shaker/src/index.ts
@@ -0,0 +1,65 @@
+import generator from '@babel/generator';
+import { transformSync } from '@babel/core';
+import type { Program } from '@babel/types';
+import { debug } from '@linaria/logger';
+import type { Evaluator, StrictOptions } from '@linaria/babel-preset';
+import { buildOptions } from '@linaria/babel-preset';
+import shake from './shaker';
+
+function prepareForShake(
+ filename: string,
+ options: StrictOptions,
+ code: string
+): Program {
+ const transformOptions = buildOptions(filename, options);
+
+ transformOptions.ast = true;
+ transformOptions.presets!.unshift([
+ require.resolve('@babel/preset-env'),
+ {
+ targets: 'ie 11',
+ // we need this plugin so we list it explicitly, explanation in `packages/extractor/src/index`
+ include: ['@babel/plugin-transform-template-literals'],
+ },
+ ]);
+ transformOptions.presets!.unshift([
+ require.resolve('@linaria/preeval'),
+ options,
+ ]);
+ transformOptions.plugins!.unshift(
+ require.resolve('babel-plugin-transform-react-remove-prop-types')
+ );
+ transformOptions.plugins!.unshift([
+ require.resolve('@babel/plugin-transform-runtime'),
+ { useESModules: false },
+ ]);
+
+ debug(
+ 'evaluator:shaker:transform',
+ `Transform ${filename} with options ${JSON.stringify(
+ transformOptions,
+ null,
+ 2
+ )}`
+ );
+ const transformed = transformSync(code, transformOptions);
+
+ if (transformed === null || !transformed.ast) {
+ throw new Error(`${filename} cannot be transformed`);
+ }
+
+ return transformed.ast.program;
+}
+
+const shaker: Evaluator = (filename, options, text, only = null) => {
+ const [shaken, imports] = shake(
+ prepareForShake(filename, options, text),
+ only
+ );
+
+ debug('evaluator:shaker:generate', `Generate shaken source code ${filename}`);
+ const { code: shakenCode } = generator(shaken!);
+ return [shakenCode, imports];
+};
+
+export default shaker;
diff --git a/@linaria/packages/shaker/src/langs/core.ts b/@linaria/packages/shaker/src/langs/core.ts
new file mode 100644
index 0000000..c0368bb
--- /dev/null
+++ b/@linaria/packages/shaker/src/langs/core.ts
@@ -0,0 +1,653 @@
+import { types as t } from '@babel/core';
+import type {
+ AssignmentExpression,
+ Block,
+ CallExpression,
+ Directive,
+ ExpressionStatement,
+ ForInStatement,
+ ForStatement,
+ Function,
+ Identifier,
+ IfStatement,
+ MemberExpression,
+ Node,
+ ObjectExpression,
+ SequenceExpression,
+ SwitchCase,
+ SwitchStatement,
+ Terminatorless,
+ TryStatement,
+ VariableDeclaration,
+ VariableDeclarator,
+ WhileStatement,
+} from '@babel/types';
+
+import { peek } from '@linaria/babel-preset';
+import type { IdentifierHandlers, Visitors } from '../types';
+import GraphBuilderState from '../GraphBuilderState';
+import ScopeManager from '../scope';
+import DepsGraph from '../DepsGraph';
+
+function isIdentifier(
+ node: Node,
+ name?: string | string[]
+): node is Identifier {
+ return (
+ t.isIdentifier(node) &&
+ (name === undefined ||
+ (Array.isArray(name) ? name.includes(node.name) : node.name === name))
+ );
+}
+
+type SideEffect = [
+ {
+ callee?: (child: CallExpression['callee']) => boolean;
+ arguments?: (child: CallExpression['arguments']) => boolean;
+ },
+ (node: CallExpression, state: GraphBuilderState) => void
+];
+
+const sideEffects: SideEffect[] = [
+ [
+ // if the first argument of forEach is required, mark forEach as required
+ {
+ callee: (node) =>
+ t.isMemberExpression(node) &&
+ t.isIdentifier(node.property) &&
+ node.property.name === 'forEach',
+ },
+ (node, state) => state.graph.addEdge(node.arguments[0], node),
+ ],
+];
+
+function getCallee(node: CallExpression): Node {
+ if (
+ t.isSequenceExpression(node.callee) &&
+ node.callee.expressions.length === 2
+ ) {
+ const [first, second] = node.callee.expressions;
+ if (t.isNumericLiteral(first) && first.value === 0) {
+ return second;
+ }
+ }
+
+ return node.callee;
+}
+
+function findWildcardReexportStatement(
+ node: t.CallExpression,
+ identifierName: string,
+ graph: DepsGraph
+): t.Statement | null {
+ if (!t.isIdentifier(node.callee) || node.callee.name !== 'require')
+ return null;
+
+ const declarator = graph.getParent(node);
+ if (!t.isVariableDeclarator(declarator)) return null;
+
+ const declaration = graph.getParent(declarator);
+ if (!t.isVariableDeclaration(declaration)) return null;
+
+ const program = graph.getParent(declaration);
+ if (!t.isProgram(program)) return null;
+
+ // Our node is a correct export
+ // Let's check that we have something that looks like transpiled re-export
+ return (
+ program.body.find((statement) => {
+ /*
+ * We are looking for `Object.keys(_bar).forEach(…)`
+ */
+
+ if (!t.isExpressionStatement(statement)) return false;
+
+ const expression = statement.expression;
+ if (!t.isCallExpression(expression)) return false;
+
+ const callee = expression.callee;
+ if (!t.isMemberExpression(callee)) return false;
+
+ const { object, property } = callee;
+
+ if (!isIdentifier(property, 'forEach')) return false;
+
+ if (!t.isCallExpression(object)) return false;
+
+ // `object` should be `Object.keys`
+ if (
+ !t.isMemberExpression(object.callee) ||
+ !isIdentifier(object.callee.object, 'Object') ||
+ !isIdentifier(object.callee.property, 'keys')
+ )
+ return false;
+
+ //
+ const [argument] = object.arguments;
+ return isIdentifier(argument, identifierName);
+ }) ?? null
+ );
+}
+
+/*
+ * Returns nodes which are implicitly affected by specified node
+ */
+function getAffectedNodes(node: Node, state: GraphBuilderState): Node[] {
+ // FIXME: this method should be generalized
+ const callee = t.isCallExpression(node) ? getCallee(node) : null;
+ if (
+ t.isCallExpression(node) &&
+ t.isMemberExpression(callee) &&
+ isIdentifier(callee.object, 'Object') &&
+ isIdentifier(callee.property, [
+ 'assign',
+ 'defineProperty',
+ 'defineProperties',
+ 'freeze',
+ 'observe',
+ ])
+ ) {
+ const [obj, property] = node.arguments;
+ if (!t.isIdentifier(obj)) {
+ return [];
+ }
+
+ if (
+ state.scope.getDeclaration(obj) !== ScopeManager.globalExportsIdentifier
+ ) {
+ return [node.arguments[0]];
+ }
+
+ if (t.isStringLiteral(property)) {
+ if (property.value === '__esModule') {
+ return [node.arguments[0]];
+ }
+
+ state.graph.addExport(property.value, node);
+ }
+ }
+
+ return [];
+}
+
+export const visitors: Visitors = {
+ /*
+ * FunctionDeclaration | FunctionExpression | ObjectMethod | ArrowFunctionExpression | ClassMethod | ClassPrivateMethod;
+ * Functions can be either a statement or an expression.
+ * That's why we need to disable default dependency resolving strategy for expressions by passing `ignoreDeps` flag.
+ * Every function must have a body. Without a body, it becomes invalid.
+ * In general, a body depends on parameters of a function.
+ * In real life, some of the parameters can be omitted, but it's not trivial to implement that type of tree shaking.
+ */
+ Function(this: GraphBuilderState, node: Function) {
+ const unsubscribe = this.onVisit((descendant) =>
+ this.graph.addEdge(node, descendant)
+ );
+ this.baseVisit(node, true); // ignoreDeps=true prevents default dependency resolving
+ unsubscribe();
+
+ this.graph.addEdge(node, node.body);
+
+ node.params.forEach((param) => this.graph.addEdge(node.body, param));
+ if (
+ t.isFunctionExpression(node) &&
+ node.id !== null &&
+ node.id !== undefined
+ ) {
+ // keep function name in expressions like `const a = function a();`
+ this.graph.addEdge(node, node.id);
+ }
+ },
+
+ /*
+ * ExpressionStatement
+ */
+ ExpressionStatement(this: GraphBuilderState, node: ExpressionStatement) {
+ this.baseVisit(node);
+
+ this.graph.addEdge(node, node.expression);
+ this.graph.addEdge(node.expression, node);
+ },
+
+ /*
+ * BlockStatement | Program
+ * The same situation as in ExpressionStatement: if one of the expressions is required, the block itself is also required.
+ * Whereas a block doesn't depend on its children.
+ * Example:
+ * 1. let c;
+ * 2. { // BlockStatement begin
+ * 3. let a = 1;
+ * 4. let b = 2;
+ * 5. a++;
+ * 6. a = c;
+ * 7. } // BlockStatement end
+ *
+ * If we want to evaluate the value of `c`, we need to evaluate lines 1, 3, 5 and 6,
+ * but we don't need line 4, even though it's a child of the block.
+ */
+ Block(this: GraphBuilderState, node: Block) {
+ this.baseVisit(node);
+
+ if (t.isProgram(node)) {
+ const exportsDeclaration = this.scope.getDeclaration('global:exports')!;
+ this.graph.addEdge(node, exportsDeclaration);
+ node.directives.forEach((directive) =>
+ this.graph.addEdge(node, directive)
+ );
+ }
+ },
+
+ Directive(this: GraphBuilderState, node: Directive) {
+ this.baseVisit(node);
+ this.graph.addEdge(node, node.value);
+ },
+
+ /*
+ * TryStatement
+ * try { /* block *\/ } catch() {/* handler *\/} finalize {/* finalizer *\/}
+ * `handler` and `finalizer` do not make sense without `block`
+ * `block` depends on the whole node.
+ */
+ TryStatement(this: GraphBuilderState, node: TryStatement) {
+ this.baseVisit(node);
+ [node.handler, node.finalizer].forEach((statement) => {
+ if (statement) {
+ this.graph.addEdge(node.block, statement);
+ this.graph.addEdge(statement, node.block);
+ }
+ });
+ },
+
+ IfStatement(this: GraphBuilderState, node: IfStatement) {
+ this.baseVisit(node);
+ this.graph.addEdge(node, node.consequent);
+ this.graph.addEdge(node, node.test);
+ },
+
+ /*
+ * WhileStatement
+ * Pretty simple behaviour here:
+ * • if body is required, the statement is required
+ * • if the statement is required, the condition is also required.
+ */
+ WhileStatement(this: GraphBuilderState, node: WhileStatement) {
+ this.baseVisit(node);
+ this.graph.addEdge(node, node.test);
+ },
+
+ SwitchCase(this: GraphBuilderState, node: SwitchCase) {
+ this.baseVisit(node);
+ node.consequent.forEach((statement) => this.graph.addEdge(statement, node));
+ if (node.test) {
+ this.graph.addEdge(node, node.test);
+ }
+ },
+
+ SwitchStatement(this: GraphBuilderState, node: SwitchStatement) {
+ this.baseVisit(node);
+ node.cases.forEach((c) => this.graph.addEdge(c, node));
+ this.graph.addEdge(node, node.discriminant);
+ },
+
+ ForStatement(this: GraphBuilderState, node: ForStatement) {
+ this.baseVisit(node);
+
+ [node.init, node.test, node.update, node.body].forEach((child) => {
+ if (child) {
+ this.graph.addEdge(node, child);
+ }
+ });
+ },
+
+ /*
+ * ForInStatement
+ * for (const k in o) { body }
+ */
+ ForInStatement(this: GraphBuilderState, node: ForInStatement) {
+ this.baseVisit(node);
+
+ if (node.body) {
+ this.graph.addEdge(node, node.body);
+ this.graph.addEdge(node.body, node.left);
+ }
+
+ this.graph.addEdge(node.left, node.right);
+ },
+
+ /*
+ * BreakStatement | ContinueStatement | ReturnStatement | ThrowStatement | YieldExpression | AwaitExpression
+ * All these nodes are required to evaluate the value of a function in which they are defined.
+ * Also, the value of these nodes depends on the argument if it is presented.
+ */
+ Terminatorless(this: GraphBuilderState, node: Terminatorless) {
+ this.baseVisit(node);
+
+ if (
+ !(t.isBreakStatement(node) || t.isContinueStatement(node)) &&
+ node.argument
+ ) {
+ this.graph.addEdge(node, node.argument);
+ }
+
+ const closestFunctionNode = peek(this.fnStack);
+ this.graph.addEdge(closestFunctionNode, node);
+ },
+
+ /*
+ * ObjectExpression
+ * Objects are… complicated. Especially because similarly looking code can be either an expression or a pattern.
+ * In this case we work with an expression like:
+ * const obj = {
+ * method() {}, // ObjectMethod
+ * property: "value", // ObjectProperty
+ * ...rest, // SpreadElement
+ * }
+ */
+ ObjectExpression(this: GraphBuilderState, node: ObjectExpression) {
+ this.context.push('expression');
+ this.baseVisit(node);
+ node.properties.forEach((prop) => {
+ this.graph.addEdge(node, prop);
+ if (t.isObjectMethod(prop)) {
+ this.graph.addEdge(prop, prop.key);
+ this.graph.addEdge(prop, prop.body);
+ } else if (t.isObjectProperty(prop)) {
+ this.graph.addEdge(prop, prop.key);
+ this.graph.addEdge(prop, prop.value);
+ } else if (t.isSpreadElement(prop)) {
+ this.graph.addEdge(prop, prop.argument);
+ }
+ });
+ this.context.pop();
+ },
+
+ /*
+ * MemberExpression
+ * It's about a simple expression like `obj.foo` or `obj['foo']`.
+ * In addition to default behaviour (an expression depends on all its children),
+ * we add a backward dependency from an object to a node for processing member
+ * expressions in assignments.
+ *
+ * Example:
+ * let obj = { a: 1 };
+ * obj.b = 2;
+ *
+ * If we try to evaluate `obj` without backward dependency,
+ * `obj.b = 2` will be cut and we will get just `{ a: 1 }`.
+ */
+ MemberExpression(this: GraphBuilderState, node: MemberExpression) {
+ this.baseVisit(node);
+
+ if (
+ isIdentifier(node.object, 'exports') &&
+ this.scope.getDeclaration(node.object) ===
+ ScopeManager.globalExportsIdentifier
+ ) {
+ // We treat `exports.something` and `exports['something']` as identifiers in the global scope
+ this.graph.addEdge(node, node.object);
+ this.graph.addEdge(node, node.property);
+
+ const isLVal = peek(this.context) === 'lval';
+ if (isLVal) {
+ this.scope.declare(node, false);
+ } else {
+ const declaration = this.scope.addReference(node);
+ this.graph.addEdge(node, declaration);
+ }
+
+ return;
+ }
+
+ if (
+ t.isIdentifier(node.object) &&
+ ((t.isIdentifier(node.property) && !node.computed) ||
+ t.isStringLiteral(node.property))
+ ) {
+ // It's simple `foo.bar` or `foo["bar"]` expression. Is it a usage of a required library?
+ const declaration = this.scope.getDeclaration(node.object);
+ if (
+ t.isIdentifier(declaration) &&
+ this.graph.importAliases.has(declaration)
+ ) {
+ // It is. We can remember what exactly we use from it.
+ const source = this.graph.importAliases.get(declaration)!;
+ this.graph.imports.get(source)!.push(node.property);
+ }
+ }
+ },
+
+ /*
+ * AssignmentExpression
+ * `a = b`, `{ ...rest } = obj`, `obj.a = 3`, etc.
+ * It's not a declaration, it's just an assignment, but it affects
+ * the value of declared variable if the variable it mentioned in the left part.
+ * So, we apply some context-magic here in order to catch reference of variables in the left part.
+ * We switch the context to `lval` and continue traversing through the left branch.
+ * If we then meet some identifier, we mark it as a dependency of its declaration.
+ */
+ AssignmentExpression(this: GraphBuilderState, node: AssignmentExpression) {
+ this.context.push('lval');
+ this.visit<AssignmentExpression['left'], AssignmentExpression>(
+ node.left,
+ node,
+ 'left'
+ );
+ this.context.pop();
+
+ this.visit(node.right, node, 'right');
+
+ // The value of an expression depends on the left part.
+ this.graph.addEdge(node, node.left);
+
+ // The left part of an assignment depends on the right part.
+ this.graph.addEdge(node.left, node.right);
+ },
+
+ /*
+ * VariableDeclarator
+ * It would be pretty simple if it weren't used to declare variables from other modules.
+ */
+ VariableDeclarator(this: GraphBuilderState, node: VariableDeclarator) {
+ /*
+ * declared is used for detecting external dependencies in cases like
+ * const { a, b, c } = require('module');
+ *
+ * We are remembering all declared variables in order to use it later in CallExpression visitor
+ */
+ const declared: Array<[Identifier, Identifier | null]> = [];
+ this.meta.set('declared', declared);
+ const unregister = this.scope.addDeclareHandler((identifier, from) =>
+ declared.push([identifier, from])
+ );
+ this.baseVisit(node);
+ this.meta.delete('declared');
+ unregister();
+
+ if (node.init) {
+ // If there is an initialization part, the identifier depends on it.
+ this.graph.addEdge(node.id, node.init);
+ }
+
+ // If a statement is required itself, an id is also required
+ this.graph.addEdge(node, node.id);
+ },
+
+ /*
+ * VariableDeclaration
+ * It's just a wrapper for group of VariableDeclarator.
+ * If one of the declarators is required, the wrapper itself is also required.
+ */
+ VariableDeclaration(this: GraphBuilderState, node: VariableDeclaration) {
+ this.meta.set('kind-of-declaration', node.kind);
+ this.baseVisit(node);
+ node.declarations.forEach((declaration) =>
+ this.graph.addEdge(declaration, node)
+ );
+ this.meta.delete('kind-of-declaration');
+ },
+
+ /*
+ * CallExpression
+ * Do you remember that we have already mentioned it in VariableDeclarator?
+ * It is a simple expression with default behaviour unless it is a `require`.
+ *
+ * Another tricky use case here is functions with side effects (e.g. `Object.defineProperty`).
+ */
+ CallExpression(
+ this: GraphBuilderState,
+ node: CallExpression,
+ parent: Node | null
+ ) {
+ this.baseVisit(node);
+
+ if (t.isIdentifier(node.callee) && node.callee.name === 'require') {
+ // It looks like a module import …
+ const scopeId = this.scope.whereIsDeclared(node.callee);
+ if (scopeId && scopeId !== 'global') {
+ // … but it is just a user defined function
+ return;
+ }
+
+ const [firstArg] = node.arguments;
+ if (!t.isStringLiteral(firstArg)) {
+ // dynamic import? Maybe someday we can do something about it
+ return;
+ }
+
+ const { value: source } = firstArg;
+ const declared = this.meta.get('declared') as Array<
+ [Identifier, Identifier | null]
+ >;
+ if (!declared) {
+ // This is a standalone `require`
+ return;
+ }
+
+ // Define all declared variables as external dependencies.
+ declared.forEach(([local, _imported]) =>
+ // FIXME: var slugify = require('../slugify').default;
+ {
+ if (!this.graph.imports.has(source)) {
+ this.graph.imports.set(source, []);
+ }
+
+ if (
+ parent &&
+ t.isMemberExpression(parent) &&
+ t.isIdentifier(parent.property)
+ ) {
+ // An imported function is specified right here.
+ // eg. require('../slugify').default
+ this.graph.imports.get(source)!.push(parent.property);
+ } else {
+ if (
+ t.isCallExpression(parent) &&
+ t.isIdentifier(parent.callee) &&
+ typeof parent.callee.name === 'string'
+ ) {
+ if (parent.callee.name.startsWith('_interopRequireDefault')) {
+ this.graph.importTypes.set(source, 'default');
+ } else if (
+ parent.callee.name.startsWith('_interopRequireWildcard')
+ ) {
+ this.graph.importTypes.set(source, 'wildcard');
+ } else {
+ // What I've missed?
+ }
+ }
+
+ // Do we know the type of import?
+ if (!this.graph.importTypes.has(source)) {
+ // Is it a wildcard reexport? Let's check.
+ const statement = findWildcardReexportStatement(
+ node,
+ local.name,
+ this.graph
+ );
+ if (statement) {
+ this.graph.addEdge(local, statement);
+ this.graph.reexports.push(local);
+ this.graph.importTypes.set(source, 'reexport');
+ }
+ }
+
+ // The whole namespace was imported. We will know later, what exactly we need.
+ // eg. const slugify = require('../slugify');
+ this.graph.importAliases.set(local, source);
+ }
+ }
+ );
+
+ return;
+ }
+
+ sideEffects.forEach(([conditions, callback]) => {
+ if (
+ (conditions.callee && !conditions.callee(node.callee)) ||
+ (conditions.arguments && !conditions.arguments(node.arguments))
+ ) {
+ return;
+ }
+
+ return callback(node, this);
+ });
+
+ getAffectedNodes(node, this).forEach((affectedNode) => {
+ this.graph.addEdge(affectedNode, node);
+ if (t.isIdentifier(affectedNode)) {
+ this.graph.addEdge(
+ this.scope.getDeclaration(affectedNode)!,
+ affectedNode
+ );
+ }
+ });
+ },
+
+ /*
+ * SequenceExpression
+ * It is a special case of expression in which the value of the whole
+ * expression depends only on the last subexpression in the list.
+ * The rest of the subexpressions can be omitted if they don't have dependent nodes.
+ *
+ * Example:
+ * const a = (1, 2, b = 3, 4, b + 2); // `a` will be equal 5
+ */
+ SequenceExpression(this: GraphBuilderState, node: SequenceExpression) {
+ // Sequence value depends on only last expression in the list
+ this.baseVisit(node, true);
+ if (node.expressions.length > 0) {
+ this.graph.addEdge(node, node.expressions[node.expressions.length - 1]);
+ }
+ },
+};
+
+export const identifierHandlers: IdentifierHandlers = {
+ declare: [
+ ['CatchClause', 'param'],
+ ['Function', 'params'],
+ ['FunctionExpression', 'id'],
+ ['RestElement', 'argument'],
+ ['ThrowStatement', 'argument'],
+ ['VariableDeclarator', 'id'],
+ ],
+ keep: [['ObjectProperty', 'key']],
+ refer: [
+ ['ArrayExpression', 'elements'],
+ ['AssignmentExpression', 'left', 'right'],
+ ['BinaryExpression', 'left', 'right'],
+ ['CallExpression', 'arguments', 'callee'],
+ ['ConditionalExpression', 'test', 'consequent', 'alternate'],
+ ['ForInStatement', 'right'],
+ ['Function', 'body'],
+ ['IfStatement', 'test'],
+ ['LogicalExpression', 'left', 'right'],
+ ['NewExpression', 'arguments', 'callee'],
+ ['ObjectProperty', 'value'],
+ ['ReturnStatement', 'argument'],
+ ['SequenceExpression', 'expressions'],
+ ['SwitchStatement', 'discriminant'],
+ ['UnaryExpression', 'argument'],
+ ['UpdateExpression', 'argument'],
+ ['VariableDeclarator', 'init'],
+ ],
+};
diff --git a/@linaria/packages/shaker/src/scope.ts b/@linaria/packages/shaker/src/scope.ts
new file mode 100644
index 0000000..2b496b0
--- /dev/null
+++ b/@linaria/packages/shaker/src/scope.ts
@@ -0,0 +1,210 @@
+import { types as t } from '@babel/core';
+import invariant from 'ts-invariant';
+
+type Scope = Map<string, Set<t.Identifier | t.MemberExpression>>;
+
+export type ScopeId = number | 'global' | 'exports';
+export type DeclareHandler = (
+ identifier: t.Identifier,
+ from: t.Identifier | null
+) => void;
+
+const ResolvedNode = Symbol('ResolvedNode');
+const functionScopes = new WeakSet<Scope>();
+
+export class PromisedNode<T = t.Node> {
+ static is<TNode>(obj: any): obj is PromisedNode<TNode> {
+ return obj && ResolvedNode in obj;
+ }
+
+ [ResolvedNode]: T | undefined;
+
+ get identifier(): T | undefined {
+ return this[ResolvedNode];
+ }
+}
+
+export const resolveNode = <T = t.Node>(
+ obj: T | PromisedNode<T> | undefined
+): T | undefined => (PromisedNode.is<T>(obj) ? obj.identifier : obj);
+
+const getExportName = (node: t.Node): string => {
+ invariant(
+ t.isMemberExpression(node),
+ `getExportName expects MemberExpression but received ${node.type}`
+ );
+
+ const { object, property } = node;
+ invariant(
+ t.isIdentifier(object) && object.name === 'exports',
+ `getExportName expects a member expression with 'exports'`
+ );
+ invariant(
+ t.isIdentifier(property) || t.isStringLiteral(property),
+ `getExportName supports only identifiers and literals as names of exported values`
+ );
+
+ const name = t.isIdentifier(property) ? property.name : property.value;
+ return `exports.${name}`;
+};
+
+const scopeIds = new WeakMap<Scope, ScopeId>();
+const getId = (scope: Scope, identifier: t.Identifier | string): string => {
+ const scopeId = scopeIds.get(scope);
+ return `${scopeId}:${
+ typeof identifier === 'string' ? identifier : identifier.name
+ }`;
+};
+
+export default class ScopeManager {
+ public static globalExportsIdentifier = t.identifier('exports');
+ public static globalModuleIdentifier = t.identifier('module');
+ private nextId = 0;
+ private readonly stack: Array<Scope> = [];
+ private readonly map: Map<ScopeId, Scope> = new Map();
+ private readonly handlers: Map<ScopeId, Array<DeclareHandler>> = new Map();
+ private readonly declarations: Map<
+ string,
+ t.Identifier | t.MemberExpression | PromisedNode<t.Identifier>
+ > = new Map();
+
+ private get global(): Scope {
+ return this.map.get('global')!;
+ }
+
+ constructor() {
+ this.new(true, 'global');
+ this.declare(ScopeManager.globalExportsIdentifier, false);
+ this.declare(ScopeManager.globalModuleIdentifier, false);
+ }
+
+ new(isFunction: boolean, scopeId: ScopeId = this.nextId++): Scope {
+ const scope: Scope = new Map();
+ if (isFunction) {
+ functionScopes.add(scope);
+ }
+
+ scopeIds.set(scope, scopeId);
+ this.map.set(scopeId, scope);
+ this.handlers.set(scopeId, []);
+ this.stack.unshift(scope);
+ return scope;
+ }
+
+ dispose(): Scope | undefined {
+ const disposed = this.stack.shift();
+ if (disposed) {
+ this.map.delete(scopeIds.get(disposed)!);
+ }
+
+ return disposed;
+ }
+
+ declare(
+ identifierOrMemberExpression: t.Identifier | t.MemberExpression,
+ isHoistable: boolean,
+ from: t.Identifier | null = null,
+ stack = 0
+ ): void {
+ if (t.isMemberExpression(identifierOrMemberExpression)) {
+ // declare receives MemberExpression only if it's `exports.something` expression
+ const memberExp = identifierOrMemberExpression;
+ const name = getExportName(memberExp);
+ if (!this.global.has(name)) {
+ this.global.set(name, new Set());
+ }
+
+ // There can be a few `export.foo = …` statements, but we need only the last one
+ this.declarations.set(getId(this.global, name), memberExp);
+ this.global.get(name)!.add(memberExp);
+ return;
+ }
+
+ const identifier = identifierOrMemberExpression;
+ const idName = identifier.name;
+ const scope = this.stack
+ .slice(stack)
+ .find((s) => !isHoistable || functionScopes.has(s))!;
+ if (this.global.has(idName)) {
+ // It's probably a declaration of a previous referenced identifier
+ // Let's use naïve implementation of hoisting
+ const promise = this.declarations.get(
+ getId(this.global, identifier)
+ )! as PromisedNode<t.Identifier>;
+ promise[ResolvedNode] = identifier;
+ scope.set(
+ idName,
+ new Set([identifier, ...Array.from(this.global.get(idName)!)])
+ );
+ this.global.delete(idName);
+ } else {
+ scope.set(idName, new Set([identifier]));
+ }
+
+ this.declarations.set(getId(scope, identifier), identifier);
+ const handlers = this.handlers.get(scopeIds.get(scope)!)!;
+ handlers.forEach((handler) => handler(identifier, from));
+ }
+
+ addReference(
+ identifierOrMemberExpression: t.Identifier | t.MemberExpression
+ ): t.Identifier | t.MemberExpression | PromisedNode {
+ const name = t.isIdentifier(identifierOrMemberExpression)
+ ? identifierOrMemberExpression.name
+ : getExportName(identifierOrMemberExpression);
+ const scope = this.stack.find((s) => s.has(name)) ?? this.global;
+ const id = getId(scope, name);
+ if (scope === this.global && !scope.has(name)) {
+ scope.set(name, new Set());
+ this.declarations.set(id, new PromisedNode());
+ }
+
+ scope.get(name)!.add(identifierOrMemberExpression);
+ return this.declarations.get(id)!;
+ }
+
+ whereIsDeclared(identifier: t.Identifier): ScopeId | undefined {
+ const name = identifier.name;
+ const scope = this.stack.find(
+ (s) => s.has(name) && s.get(name)!.has(identifier)
+ );
+ if (scope) {
+ return scopeIds.get(scope);
+ }
+
+ if (this.global.has(name)) {
+ return 'global';
+ }
+
+ return undefined;
+ }
+
+ getDeclaration(
+ identifierOrMemberExpOrName: t.Identifier | t.MemberExpression | string
+ ): t.Identifier | t.MemberExpression | undefined {
+ let name: string;
+ if (typeof identifierOrMemberExpOrName === 'string') {
+ name = identifierOrMemberExpOrName;
+ } else if (t.isMemberExpression(identifierOrMemberExpOrName)) {
+ name = getId(this.global, getExportName(identifierOrMemberExpOrName));
+ } else {
+ const scopeId = this.whereIsDeclared(identifierOrMemberExpOrName);
+ if (scopeId === undefined) {
+ return undefined;
+ }
+
+ name = getId(this.map.get(scopeId)!, identifierOrMemberExpOrName);
+ }
+
+ return resolveNode(this.declarations.get(name));
+ }
+
+ addDeclareHandler(handler: DeclareHandler): () => void {
+ const scopeId = scopeIds.get(this.stack[0])!;
+ this.handlers.get(scopeId)!.push(handler);
+ return () => {
+ const handlers = this.handlers.get(scopeId)!.filter((h) => h !== handler);
+ this.handlers.set(scopeId, handlers);
+ };
+ }
+}
diff --git a/@linaria/packages/shaker/src/shaker.ts b/@linaria/packages/shaker/src/shaker.ts
new file mode 100644
index 0000000..f2584f3
--- /dev/null
+++ b/@linaria/packages/shaker/src/shaker.ts
@@ -0,0 +1,127 @@
+import type { Node, Program } from '@babel/types';
+import generator from '@babel/generator';
+import { debug } from '@linaria/logger';
+import { isNode, getVisitorKeys } from '@linaria/babel-preset';
+import build from './graphBuilder';
+import dumpNode from './dumpNode';
+
+/*
+ * Returns new tree without dead nodes
+ */
+function shakeNode<TNode extends Node>(node: TNode, alive: Set<Node>): Node {
+ const keys = getVisitorKeys(node) as Array<keyof TNode>;
+ const changes: Partial<TNode> = {};
+ const isNodeAlive = (n: Node) => alive.has(n);
+
+ for (const key of keys) {
+ const subNode = node[key];
+
+ if (Array.isArray(subNode)) {
+ const list: any = [];
+ let hasChanges = false;
+ for (let i = 0; i < subNode.length; i++) {
+ const child = subNode[i];
+ const isAlive = isNodeAlive(child);
+ hasChanges = hasChanges || !isAlive;
+ if (child && isAlive) {
+ const shaken = shakeNode(child, alive);
+ if (shaken) {
+ list.push(shaken);
+ }
+
+ hasChanges = hasChanges || shaken !== child;
+ }
+ }
+ if (hasChanges) {
+ changes[key] = list;
+ }
+ } else if (isNode(subNode)) {
+ if (isNodeAlive(subNode)) {
+ const shaken = shakeNode(subNode, alive);
+ if (shaken && shaken !== subNode) {
+ changes[key] = shaken as any;
+ }
+ } else {
+ changes[key] = undefined;
+ }
+ }
+ }
+
+ return Object.keys(changes).length ? { ...node, ...changes } : node;
+}
+
+/*
+ * Gets AST and a list of nodes for evaluation
+ * Removes unrelated “dead” code.
+ * Adds to the end of module export of array of evaluated values or evaluation errors.
+ * Returns new AST and an array of external dependencies.
+ */
+export default function shake(
+ rootPath: Program,
+ exports: string[] | null
+): [Program, Map<string, string[]>] {
+ debug(
+ 'evaluator:shaker:shake',
+ () =>
+ `source (exports: ${(exports || []).join(', ')}):\n${
+ generator(rootPath).code
+ }`
+ );
+
+ const depsGraph = build(rootPath);
+ const alive = new Set<Node>();
+ const reexports: string[] = [];
+ let deps = (exports ?? [])
+ .map((token) => {
+ const node = depsGraph.getLeaf(token);
+ if (node) return [node];
+ // We have some unknown token. Do we have `export * from …` in that file?
+ if (depsGraph.reexports.length === 0) {
+ return [];
+ }
+
+ // If so, mark all re-exported files as required
+ reexports.push(token);
+ return [...depsGraph.reexports];
+ })
+ .reduce<Node[]>((acc, el) => {
+ acc.push(...el);
+ return acc;
+ }, []);
+ while (deps.length > 0) {
+ // Mark all dependencies as alive
+ deps.forEach((d) => alive.add(d));
+
+ // Collect new dependencies of dependencies
+ deps = depsGraph.getDependencies(deps).filter((d) => !alive.has(d));
+ }
+
+ const shaken = shakeNode(rootPath, alive) as Program;
+ /*
+ * If we want to know what is really happen with our code tree,
+ * we can print formatted tree here by setting env variable LINARIA_LOG=debug
+ */
+ debug('evaluator:shaker:shake', () => dumpNode(rootPath, alive));
+
+ const imports = new Map<string, string[]>();
+ for (let [source, members] of depsGraph.imports.entries()) {
+ const importType = depsGraph.importTypes.get(source);
+ const defaultMembers = importType === 'wildcard' ? ['*'] : [];
+ const aliveMembers = new Set(
+ members
+ .filter((i) => alive.has(i))
+ .map((i) => (i.type === 'Identifier' ? i.name : i.value))
+ );
+
+ if (importType === 'reexport') {
+ reexports.forEach((token) => aliveMembers.add(token));
+ }
+
+ imports.set(
+ source,
+ aliveMembers.size > 0 ? Array.from(aliveMembers) : defaultMembers
+ );
+ }
+
+ return [shaken, imports];
+}
diff --git a/@linaria/packages/shaker/src/types.ts b/@linaria/packages/shaker/src/types.ts
new file mode 100644
index 0000000..582e014
--- /dev/null
+++ b/@linaria/packages/shaker/src/types.ts
@@ -0,0 +1,22 @@
+import type { Aliases, Node, VisitorKeys } from '@babel/types';
+
+export type NodeOfType<T> = Extract<Node, { type: T }>;
+
+export type NodeType = Node['type'] | keyof Aliases;
+
+export type VisitorAction = 'ignore' | void;
+
+export type Visitor<TNode extends Node> = <TParent extends Node>(
+ node: TNode,
+ parent: TParent | null,
+ parentKey: VisitorKeys[TParent['type']] | null,
+ listIdx: number | null
+) => VisitorAction;
+
+export type Visitors = { [TMethod in NodeType]?: Visitor<NodeOfType<TMethod>> };
+
+export type IdentifierHandlerType = 'declare' | 'keep' | 'refer';
+
+export type IdentifierHandlers = {
+ [key in IdentifierHandlerType]: [NodeType, ...string[]][];
+};
diff --git a/@linaria/packages/shaker/tsconfig.json b/@linaria/packages/shaker/tsconfig.json
new file mode 100644
index 0000000..fbcbab1
--- /dev/null
+++ b/@linaria/packages/shaker/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": { "paths": {}, "rootDir": "src/" },
+ "references": [
+ { "path": "../babel" },
+ { "path": "../logger" },
+ { "path": "../preeval" }
+ ]
+}