1import { transformSync as transform } from '@babel/core';
2import { createFilter } from 'rollup-pluginutils';
3
4function unwrapStatePlugin({ types: t }) {
5 return {
6 pre() {
7 this.props = new Map();
8 this.test = (node) =>
9 /state$/i.test(node.id.name) ||
10 (node.init.properties.length === 1 && node.init.properties[0].key.name === 'contents');
11 },
12 visitor: {
13 VariableDeclarator(path) {
14 if (
15 t.isIdentifier(path.node.id) &&
16 t.isObjectExpression(path.node.init) &&
17 path.node.init.properties.every(
18 (prop) => t.isObjectProperty(prop) && t.isIdentifier(prop.key)
19 ) &&
20 this.test(path.node)
21 ) {
22 const id = path.node.id.name;
23 const properties = path.node.init.properties;
24 const propNames = new Set(properties.map((x) => x.key.name));
25 const decl = properties.map((prop) => {
26 const key = `${id}$${prop.key.name}`;
27 return t.variableDeclarator(t.identifier(key), prop.value);
28 });
29
30 this.props.set(id, propNames);
31 path.parentPath.replaceWithMultiple(t.variableDeclaration('let', decl));
32 }
33 },
34 MemberExpression(path) {
35 if (
36 t.isIdentifier(path.node.object) &&
37 this.props.has(path.node.object.name) &&
38 t.isIdentifier(path.node.property) &&
39 this.props.get(path.node.object.name).has(path.node.property.name)
40 ) {
41 const id = path.node.object.name;
42 const propName = path.node.property.name;
43 path.replaceWith(t.identifier(`${id}$${propName}`));
44 }
45 },
46 },
47 };
48}
49
50function curryGuaranteePlugin({ types: t }) {
51 const curryFnName = /^_(\d)$/;
52 const lengthId = t.identifier('length');
53 const bindId = t.identifier('bind');
54
55 return {
56 visitor: {
57 CallExpression(path) {
58 if (!t.isIdentifier(path.node.callee) || !curryFnName.test(path.node.callee.name)) {
59 return;
60 }
61
62 const callFn = path.node.arguments[0];
63 const callArgs = path.node.arguments.slice(1);
64
65 // Check whether the value of the call is unused
66 if (t.isExpressionStatement(path.parent)) {
67 path.replaceWith(t.callExpression(callFn, callArgs));
68 return;
69 }
70
71 // Check whether the callee is a local function definition whose arity matches
72 if (t.isIdentifier(callFn) && path.scope.hasBinding(callFn.name)) {
73 const callFnDefinition = path.scope.getBinding(callFn.name).path.node;
74 if (
75 t.isFunctionDeclaration(callFnDefinition) &&
76 callFnDefinition.params.length === callArgs.length
77 ) {
78 path.replaceWith(t.callExpression(callFn, callArgs));
79 return;
80 }
81 }
82
83 // Special case since sources don't return any value
84 if (
85 t.isIdentifier(callFn) &&
86 callFn.name === 'source' &&
87 t.isReturnStatement(path.parent)
88 ) {
89 path.replaceWith(t.callExpression(callFn, callArgs));
90 return;
91 }
92
93 const arityLiteral = t.numericLiteral(callArgs.length);
94 const argIds = callArgs.map((init) => {
95 if (t.isIdentifier(init)) return init;
96 const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id);
97 path.scope.push({ id, init });
98 return id;
99 });
100
101 path.replaceWith(
102 t.conditionalExpression(
103 t.binaryExpression('===', t.memberExpression(callFn, lengthId), arityLiteral),
104 t.callExpression(callFn, argIds),
105 t.callExpression(t.memberExpression(callFn, bindId), [t.nullLiteral()].concat(argIds))
106 )
107 );
108 },
109 },
110 };
111}
112
113function squashImplicitUnitReturn({ types: t }) {
114 return {
115 visitor: {
116 ReturnStatement(path) {
117 if (
118 t.isCallExpression(path.node.argument) &&
119 t.isIdentifier(path.node.argument.callee) &&
120 (path.node.argument.callee.name === 'sink' || path.node.argument.callee.name === 'source')
121 ) {
122 path.replaceWithMultiple([
123 t.expressionStatement(path.node.argument),
124 t.returnStatement(),
125 ]);
126 }
127 },
128 Function: {
129 exit(functionPath) {
130 if (t.isIdentifier(functionPath.id) && functionPath.id.name === 'valFromOption') return;
131
132 let hasEmptyReturn = false;
133 let hasCallReturnOnly = true;
134 functionPath.traverse({
135 Function(innerPath) {
136 innerPath.skip();
137 },
138 ReturnStatement: {
139 enter(path) {
140 if (path.node.argument === null) {
141 hasEmptyReturn = true;
142 } else if (!t.isCallExpression(path.node.argument)) {
143 hasCallReturnOnly = false;
144 }
145 },
146 exit(path) {
147 if (hasEmptyReturn && hasCallReturnOnly && path.node.argument !== null) {
148 path.replaceWithMultiple([
149 t.expressionStatement(path.node.argument),
150 t.returnStatement(),
151 ]);
152 }
153 },
154 },
155 });
156 },
157 },
158 },
159 };
160}
161
162function cleanup(opts = {}) {
163 const filter = createFilter(opts.include, opts.exclude, {
164 resolve: false,
165 });
166
167 return {
168 name: 'minifyBucklescript',
169
170 renderChunk(code, chunk) {
171 if (!filter(chunk.fileName)) {
172 return null;
173 }
174
175 return transform(code, {
176 plugins: [unwrapStatePlugin, curryGuaranteePlugin, squashImplicitUnitReturn],
177 babelrc: false,
178 });
179 },
180 };
181}
182
183export default cleanup;