1 | 'use strict';
|
2 |
|
3 | const Filter = require('broccoli-persistent-filter');
|
4 | const clone = require('clone');
|
5 | const fs = require('fs');
|
6 | const stringify = require('json-stable-stringify');
|
7 | const mergeTrees = require('broccoli-merge-trees');
|
8 | const funnel = require('broccoli-funnel');
|
9 | const crypto = require('crypto');
|
10 | const hashForDep = require('hash-for-dep');
|
11 | const transformString = require('./lib/parallel-api').transformString;
|
12 |
|
13 |
|
14 | function getExtensionsRegex(extensions) {
|
15 | return extensions.map(extension => {
|
16 | return new RegExp('\.' + extension + '$');
|
17 | });
|
18 | }
|
19 |
|
20 | function 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 |
|
28 | module.exports = Babel;
|
29 | function 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 |
|
57 |
|
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 |
|
72 | Babel.prototype = Object.create(Filter.prototype);
|
73 | Babel.prototype.constructor = Babel;
|
74 | Babel.prototype.targetExtension = 'js';
|
75 |
|
76 | Babel.prototype.baseDir = function() {
|
77 | return __dirname;
|
78 | };
|
79 |
|
80 | Babel.prototype.transform = function(string, options) {
|
81 | return transformString(string, options);
|
82 | };
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 | Babel.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 |
|
130 |
|
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 |
|
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 |
|
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 |
|
169 | Babel.prototype.cacheKeyProcessString = function(string, relativePath) {
|
170 | return this.optionsHash() + Filter.prototype.cacheKeyProcessString.call(this, string, relativePath);
|
171 | };
|
172 |
|
173 | Babel.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 |
|
198 | Babel.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 |
|
209 | function 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 | }
|