summaryrefslogtreecommitdiff
path: root/@linaria/packages/shaker/src
diff options
context:
space:
mode:
Diffstat (limited to '@linaria/packages/shaker/src')
-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
11 files changed, 1743 insertions, 0 deletions
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[]][];
+};