diff options
Diffstat (limited to '@linaria/packages/shaker/src/graphBuilder.ts')
-rw-r--r-- | @linaria/packages/shaker/src/graphBuilder.ts | 192 |
1 files changed, 192 insertions, 0 deletions
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; |