UNPKG

7.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;
12
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 delete options.console;
46
47 this.options = options;
48 this.extensions = this.options.filterExtensions || ['js'];
49 this.extensionsRegex = getExtensionsRegex(this.extensions);
50 this.name = 'broccoli-babel-transpiler';
51
52 if (this.options.helperWhiteList) {
53 this.helperWhiteList = this.options.helperWhiteList;
54 }
55
56 // Note, Babel does not support this option so we must save it then
57 // delete it from the options hash
58 delete this.options.helperWhiteList;
59
60 if (this.options.browserPolyfill) {
61 let babelCorePath = require.resolve('babel-core');
62 babelCorePath = babelCorePath.replace(/\/babel-core\/.*$/, '/babel-core');
63
64 let polyfill = funnel(babelCorePath, { files: ['browser-polyfill.js'] });
65 this.inputTree = mergeTrees([polyfill, inputTree]);
66 } else {
67 this.inputTree = inputTree;
68 }
69 delete this.options.browserPolyfill;
70}
71
72Babel.prototype = Object.create(Filter.prototype);
73Babel.prototype.constructor = Babel;
74Babel.prototype.targetExtension = 'js';
75
76Babel.prototype.baseDir = function() {
77 return __dirname;
78};
79
80Babel.prototype.transform = function(string, options) {
81 return transformString(string, options);
82};
83
84/*
85 * @private
86 *
87 * @method optionsString
88 * @returns a stringified version of the input options
89 */
90Babel.prototype.optionsHash = function() {
91 let options = this.options;
92 let hash = {};
93 let key, value;
94
95 if (!this._optionsHash) {
96 for (key in options) {
97 value = options[key];
98 hash[key] = (typeof value === 'function') ? (value + '') : value;
99 }
100
101 if (options.plugins) {
102 hash.plugins = [];
103
104 let cacheableItems = options.plugins.slice();
105
106 for (let i = 0; i < cacheableItems.length; i++) {
107 let item = cacheableItems[i];
108
109 let type = typeof item;
110 let augmentsCacheKey = false;
111 let providesBaseDir = false;
112 let requiresBaseDir = true;
113
114 if (type === 'function') {
115 augmentsCacheKey = typeof item.cacheKey === 'function';
116 providesBaseDir = typeof item.baseDir === 'function';
117
118 if (augmentsCacheKey) {
119 hash.plugins.push(item.cacheKey());
120 }
121
122 if (providesBaseDir) {
123 let depHash = hashForDep(item.baseDir());
124
125 hash.plugins.push(depHash);
126 }
127
128 if (!providesBaseDir && requiresBaseDir){
129 // prevent caching completely if the plugin doesn't provide baseDir
130 // we cannot ensure that we aren't causing invalid caching pain...
131 this.console.warn('broccoli-babel-transpiler is opting out of caching due to a plugin that does not provide a caching strategy: `' + item + '`.');
132 hash.plugins.push((new Date).getTime() + '|' + Math.random());
133 break;
134 }
135 } else if (Array.isArray(item)) {
136 item.forEach(part => cacheableItems.push(part));
137 continue;
138 } else if (type !== 'object' || item === null) {
139 // handle native strings, numbers, or null (which can JSON.stringify properly)
140 hash.plugins.push(item);
141 continue;
142 } else if (type === 'object' && (typeof item.baseDir === 'function')) {
143 hash.plugins.push(hashForDep(item.baseDir()));
144
145 if (typeof item.cacheKey === 'function') {
146 hash.plugins.push(item.cacheKey());
147 }
148 } else if (type === 'object') {
149 // iterate all keys in the item and push them into the cache
150 Object.keys(item).forEach(key => {
151 cacheableItems.push(key);
152 cacheableItems.push(item[key]);
153 });
154 continue;
155 } else {
156 this.console.warn('broccoli-babel-transpiler is opting out of caching due to an non-cacheable item: `' + item + '` (' + type + ').');
157 hash.plugins.push((new Date).getTime() + '|' + Math.random());
158 break;
159 }
160 }
161 }
162
163 this._optionsHash = crypto.createHash('md5').update(stringify(hash), 'utf8').digest('hex');
164 }
165
166 return this._optionsHash;
167};
168
169Babel.prototype.cacheKeyProcessString = function(string, relativePath) {
170 return this.optionsHash() + Filter.prototype.cacheKeyProcessString.call(this, string, relativePath);
171};
172
173Babel.prototype.processString = function(string, relativePath) {
174 let options = this.copyOptions();
175
176 options.filename = options.sourceMapTarget = options.sourceFileName = relativePath;
177
178 if (options.moduleId === true) {
179 options.moduleId = replaceExtensions(this.extensionsRegex, options.filename);
180 }
181
182 let plugin = this;
183 return this.transform(string, options)
184 .then(transpiled => {
185
186 if (plugin.helperWhiteList) {
187 let invalidHelpers = transpiled.metadata.usedHelpers.filter(helper => {
188 return plugin.helperWhiteList.indexOf(helper) === -1;
189 }, plugin);
190
191 validateHelpers(invalidHelpers, relativePath);
192 }
193
194 return transpiled.code;
195 });
196};
197
198Babel.prototype.copyOptions = function() {
199 let cloned = clone(this.options);
200 if (cloned.filterExtensions) {
201 delete cloned.filterExtensions;
202 }
203 if (cloned.targetExtension) {
204 delete cloned.targetExtension;
205 }
206 return cloned;
207};
208
209function validateHelpers(invalidHelpers, relativePath) {
210 if (invalidHelpers.length > 0) {
211 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.';
212
213 if (invalidHelpers.length > 1) {
214 let helpers = invalidHelpers.map((item, i) => {
215 if (i === invalidHelpers.length - 1) {
216 return '& `' + item;
217 } else if (i === invalidHelpers.length - 2) {
218 return item + '`, ';
219 }
220
221 return item + '`, `';
222 }).join('');
223
224 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.';
225 }
226
227 throw new Error(message);
228 }
229}