UNPKG

8.28 kBJavaScriptView Raw
1'use strict';
2
3const Filter = require('broccoli-persistent-filter');
4const clone = require('clone');
5const fs = require('fs');
6const stringify = require('json-stable-stringify');
7const mergeTrees = require('broccoli-merge-trees');
8const funnel = require('broccoli-funnel');
9const crypto = require('crypto');
10const hashForDep = require('hash-for-dep');
11const transformString = require('./lib/parallel-api').transformString;
12const transformIsParallelizable = require('./lib/parallel-api').transformIsParallelizable;
13
14function getExtensionsRegex(extensions) {
15 return extensions.map(extension => {
16 return new RegExp('\.' + extension + '$');
17 });
18}
19
20function replaceExtensions(extensionsRegex, name) {
21 for (let i = 0, l = extensionsRegex.length; i < l; i++) {
22 name = name.replace(extensionsRegex[i], '');
23 }
24
25 return name;
26}
27
28module.exports = Babel;
29function Babel(inputTree, _options) {
30 if (!(this instanceof Babel)) {
31 return new Babel(inputTree, _options);
32 }
33
34 let options = _options || {};
35 options.persist = 'persist' in options ? options.persist : true;
36 options.async = true;
37 Filter.call(this, inputTree, options);
38
39 delete options.persist;
40 delete options.async;
41 delete options.annotation;
42 delete options.description;
43
44 this.console = options.console || console;
45 this.throwUnlessParallelizable = options.throwUnlessParallelizable;
46
47 delete options.console;
48 delete options.throwUnlessParallelizable;
49
50 this.options = options;
51 this.extensions = this.options.filterExtensions || ['js'];
52 this.extensionsRegex = getExtensionsRegex(this.extensions);
53 this.name = 'broccoli-babel-transpiler';
54
55 if (this.options.helperWhiteList) {
56 this.helperWhiteList = this.options.helperWhiteList;
57 }
58
59 // Note, Babel does not support this option so we must save it then
60 // delete it from the options hash
61 delete this.options.helperWhiteList;
62
63 if (this.options.browserPolyfill) {
64 let babelCorePath = require.resolve('babel-core');
65 babelCorePath = babelCorePath.replace(/\/babel-core\/.*$/, '/babel-core');
66
67 let polyfill = funnel(babelCorePath, { files: ['browser-polyfill.js'] });
68 this.inputTree = mergeTrees([polyfill, inputTree]);
69 } else {
70 this.inputTree = inputTree;
71 }
72 delete this.options.browserPolyfill;
73
74 let result = transformIsParallelizable(options);
75 let isParallelizable = result.isParallelizable;
76 let errors = result.errors;
77
78 if ((this.throwUnlessParallelizable || process.env.THROW_UNLESS_PARALLELIZABLE) && isParallelizable === false) {
79 try {
80 throw new Error(this.toString() +
81 ' was configured to `throwUnlessParallelizable` and was unable to parallelize a plugin. \nplugins:\n' + joinCount(errors) + '\nPlease see: https://github.com/babel/broccoli-babel-transpiler#parallel-transpilation for more details');
82 } catch(e) {
83 debugger;
84
85 let result = transformIsParallelizable(options);
86 throw e;
87 }
88 }
89}
90
91function joinCount(list) {
92 let summary = '';
93
94 for (let i = 0; i < list.length; i++) {
95 summary += `${i + 1}: ${list[i]}\n`
96 }
97
98 return summary;
99}
100
101Babel.prototype = Object.create(Filter.prototype);
102Babel.prototype.constructor = Babel;
103Babel.prototype.targetExtension = 'js';
104
105Babel.prototype.baseDir = function() {
106 return __dirname;
107};
108
109Babel.prototype.transform = function(string, options) {
110 return transformString(string, options);
111};
112
113/*
114 * @private
115 *
116 * @method optionsString
117 * @returns a stringified version of the input options
118 */
119Babel.prototype.optionsHash = function() {
120 let options = this.options;
121 let hash = {};
122 let key, value;
123
124 if (!this._optionsHash) {
125 for (key in options) {
126 value = options[key];
127 hash[key] = (typeof value === 'function') ? (value + '') : value;
128 }
129
130 if (options.plugins) {
131 hash.plugins = [];
132
133 let cacheableItems = options.plugins.slice();
134
135 for (let i = 0; i < cacheableItems.length; i++) {
136 let item = cacheableItems[i];
137
138 let type = typeof item;
139 let augmentsCacheKey = false;
140 let providesBaseDir = false;
141 let requiresBaseDir = true;
142
143 if (type === 'function') {
144 augmentsCacheKey = typeof item.cacheKey === 'function';
145 providesBaseDir = typeof item.baseDir === 'function';
146
147 if (augmentsCacheKey) {
148 hash.plugins.push(item.cacheKey());
149 }
150
151 if (providesBaseDir) {
152 let depHash = hashForDep(item.baseDir());
153
154 hash.plugins.push(depHash);
155 }
156
157 if (!providesBaseDir && requiresBaseDir){
158 // prevent caching completely if the plugin doesn't provide baseDir
159 // we cannot ensure that we aren't causing invalid caching pain...
160 this.console.warn('broccoli-babel-transpiler is opting out of caching due to a plugin that does not provide a caching strategy: `' + item + '`.');
161 hash.plugins.push((new Date).getTime() + '|' + Math.random());
162 break;
163 }
164 } else if (Array.isArray(item)) {
165 item.forEach(part => cacheableItems.push(part));
166 continue;
167 } else if (type !== 'object' || item === null) {
168 // handle native strings, numbers, or null (which can JSON.stringify properly)
169 hash.plugins.push(item);
170 continue;
171 } else if (type === 'object' && (typeof item.baseDir === 'function')) {
172 hash.plugins.push(hashForDep(item.baseDir()));
173
174 if (typeof item.cacheKey === 'function') {
175 hash.plugins.push(item.cacheKey());
176 }
177 } else if (type === 'object') {
178 // iterate all keys in the item and push them into the cache
179 Object.keys(item).forEach(key => {
180 cacheableItems.push(key);
181 cacheableItems.push(item[key]);
182 });
183 continue;
184 } else {
185 this.console.warn('broccoli-babel-transpiler is opting out of caching due to an non-cacheable item: `' + item + '` (' + type + ').');
186 hash.plugins.push((new Date).getTime() + '|' + Math.random());
187 break;
188 }
189 }
190 }
191
192 this._optionsHash = crypto.createHash('md5').update(stringify(hash), 'utf8').digest('hex');
193 }
194
195 return this._optionsHash;
196};
197
198Babel.prototype.cacheKeyProcessString = function(string, relativePath) {
199 return this.optionsHash() + Filter.prototype.cacheKeyProcessString.call(this, string, relativePath);
200};
201
202Babel.prototype.processString = function(string, relativePath) {
203 let options = this.copyOptions();
204
205 options.filename = options.sourceMapTarget = options.sourceFileName = relativePath;
206
207 if (options.moduleId === true) {
208 options.moduleId = replaceExtensions(this.extensionsRegex, options.filename);
209 }
210
211 return this.transform(string, options)
212 .then(transpiled => {
213
214 if (this.helperWhiteList) {
215 let invalidHelpers = transpiled.metadata.usedHelpers.filter(helper => {
216 return this.helperWhiteList.indexOf(helper) === -1;
217 });
218
219 validateHelpers(invalidHelpers, relativePath);
220 }
221
222 return transpiled.code;
223 });
224};
225
226Babel.prototype.copyOptions = function() {
227 let cloned = clone(this.options);
228 if (cloned.filterExtensions) {
229 delete cloned.filterExtensions;
230 }
231 if (cloned.targetExtension) {
232 delete cloned.targetExtension;
233 }
234 return cloned;
235};
236
237function validateHelpers(invalidHelpers, relativePath) {
238 if (invalidHelpers.length > 0) {
239 let message = relativePath + ' was transformed and relies on `' + invalidHelpers[0] + '`, which was not included in the helper whitelist. Either add this helper to the whitelist or refactor to not be dependent on this runtime helper.';
240
241 if (invalidHelpers.length > 1) {
242 let helpers = invalidHelpers.map((item, i) => {
243 if (i === invalidHelpers.length - 1) {
244 return '& `' + item;
245 } else if (i === invalidHelpers.length - 2) {
246 return item + '`, ';
247 }
248
249 return item + '`, `';
250 }).join('');
251
252 message = relativePath + ' was transformed and relies on `' + helpers + '`, which were not included in the helper whitelist. Either add these helpers to the whitelist or refactor to not be dependent on these runtime helpers.';
253 }
254
255 throw new Error(message);
256 }
257}