1 | 'use strict';
|
2 | const fs = require('fs');
|
3 | const path = require('path');
|
4 | const babel = require('@babel/core');
|
5 | const concordance = require('concordance');
|
6 | const convertSourceMap = require('convert-source-map');
|
7 | const findUp = require('find-up');
|
8 | const isPlainObject = require('is-plain-object');
|
9 | const md5Hex = require('md5-hex');
|
10 | const packageHash = require('package-hash');
|
11 | const pkgConf = require('pkg-conf');
|
12 | const stripBomBuf = require('strip-bom-buf');
|
13 | const writeFileAtomic = require('write-file-atomic');
|
14 | const pkg = require('../package.json');
|
15 |
|
16 | function getSourceMap(filePath, code) {
|
17 | let sourceMap = convertSourceMap.fromSource(code);
|
18 |
|
19 | if (!sourceMap) {
|
20 | const dirPath = path.dirname(filePath);
|
21 | sourceMap = convertSourceMap.fromMapFileSource(code, dirPath);
|
22 | }
|
23 |
|
24 | return sourceMap ? sourceMap.toObject() : undefined;
|
25 | }
|
26 |
|
27 | function hasValidKeys(conf) {
|
28 | return Object.keys(conf).every(key => key === 'extensions' || key === 'testOptions');
|
29 | }
|
30 |
|
31 | function isValidExtensions(extensions) {
|
32 | return Array.isArray(extensions) && extensions.every(ext => typeof ext === 'string' && ext !== '');
|
33 | }
|
34 |
|
35 | function validate(conf) {
|
36 | if (conf === false) {
|
37 | return null;
|
38 | }
|
39 |
|
40 | const defaultOptions = {babelrc: true, configFile: true};
|
41 |
|
42 | if (conf === undefined) {
|
43 | return {testOptions: defaultOptions};
|
44 | }
|
45 |
|
46 | if (
|
47 | !isPlainObject(conf) ||
|
48 | !hasValidKeys(conf) ||
|
49 | (conf.testOptions !== undefined && !isPlainObject(conf.testOptions)) ||
|
50 | (conf.extensions !== undefined && !isValidExtensions(conf.extensions))
|
51 | ) {
|
52 | throw new Error(`Unexpected Babel configuration for AVA. See https://github.com/avajs/ava/blob/v${pkg.version}/docs/recipes/babel.md for allowed values.`);
|
53 | }
|
54 |
|
55 | return {
|
56 | extensions: conf.extensions,
|
57 | testOptions: {...defaultOptions, ...conf.testOptions}
|
58 | };
|
59 | }
|
60 |
|
61 |
|
62 |
|
63 | function makeValueChecker(ref) {
|
64 | const expected = require(ref);
|
65 | return ({value}) => value === expected || value === expected.default;
|
66 | }
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | function createConfigItem(ref, type, options = {}) {
|
72 | return babel.createConfigItem([require.resolve(ref), options], {type});
|
73 | }
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | function wantsStage4(userOptions, projectDir) {
|
82 | if (!userOptions) {
|
83 | return false;
|
84 | }
|
85 |
|
86 | if (!userOptions.testOptions.presets) {
|
87 | return true;
|
88 | }
|
89 |
|
90 | const stage4 = require('../stage-4');
|
91 | return userOptions.testOptions.presets.every(arr => {
|
92 | if (!Array.isArray(arr)) {
|
93 | return true;
|
94 | }
|
95 |
|
96 | const [ref, options] = arr;
|
97 |
|
98 |
|
99 | const resolved = require(babel.resolvePreset(ref, projectDir));
|
100 | return resolved !== stage4 || options !== false;
|
101 | });
|
102 | }
|
103 |
|
104 | function hashPartialTestConfig({babelrc, config, options: {plugins, presets}}, projectDir, pluginAndPresetHashes) {
|
105 | const inputs = [];
|
106 | if (babelrc) {
|
107 | inputs.push(babelrc);
|
108 |
|
109 | const filename = path.basename(babelrc);
|
110 | if (filename === 'package.json') {
|
111 | inputs.push(JSON.stringify(pkgConf.sync('babel', {cwd: path.dirname(filename)})));
|
112 | } else {
|
113 | inputs.push(stripBomBuf(fs.readFileSync(babelrc)));
|
114 | }
|
115 | }
|
116 |
|
117 | if (config) {
|
118 | inputs.push(config, stripBomBuf(fs.readFileSync(config)));
|
119 | }
|
120 |
|
121 | for (const {file: {resolved: filename}} of [...plugins, ...presets]) {
|
122 | if (pluginAndPresetHashes.has(filename)) {
|
123 | inputs.push(pluginAndPresetHashes.get(filename));
|
124 | continue;
|
125 | }
|
126 |
|
127 | const [firstComponent] = path.relative(projectDir, filename).split(path.sep);
|
128 | let hash;
|
129 | if (firstComponent === 'node_modules') {
|
130 | hash = packageHash.sync(findUp.sync('package.json', {cwd: path.dirname(filename)}));
|
131 | } else {
|
132 | hash = md5Hex(stripBomBuf(fs.readFileSync(filename)));
|
133 | }
|
134 |
|
135 | pluginAndPresetHashes.set(filename, hash);
|
136 | inputs.push(hash);
|
137 | }
|
138 |
|
139 | return md5Hex(inputs);
|
140 | }
|
141 |
|
142 | function build(projectDir, cacheDir, userOptions, compileEnhancements) {
|
143 | if (!userOptions && !compileEnhancements) {
|
144 | return null;
|
145 | }
|
146 |
|
147 |
|
148 |
|
149 |
|
150 | const envName = process.env.BABEL_ENV || ('NODE_ENV' in process.env ? process.env.NODE_ENV : 'test') || 'development';
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 | const seedInputs = [
|
160 | process.versions.node,
|
161 | packageHash.sync(require.resolve('../package.json')),
|
162 | projectDir,
|
163 | envName,
|
164 | concordance.serialize(concordance.describe(userOptions))
|
165 | ];
|
166 |
|
167 | const partialCacheKey = md5Hex(seedInputs);
|
168 | const pluginAndPresetHashes = new Map();
|
169 |
|
170 | const ensureStage4 = wantsStage4(userOptions, projectDir);
|
171 | const containsAsyncGenerators = makeValueChecker('@babel/plugin-syntax-async-generators');
|
172 | const containsObjectRestSpread = makeValueChecker('@babel/plugin-syntax-object-rest-spread');
|
173 | const containsOptionalCatchBinding = makeValueChecker('@babel/plugin-syntax-optional-catch-binding');
|
174 | const containsStage4 = makeValueChecker('../stage-4');
|
175 | const containsTransformTestFiles = makeValueChecker('@ava/babel-preset-transform-test-files');
|
176 |
|
177 | const loadOptions = filename => {
|
178 | const partialTestConfig = babel.loadPartialConfig({
|
179 | babelrc: false,
|
180 | babelrcRoots: [projectDir],
|
181 | configFile: false,
|
182 | sourceMaps: true,
|
183 | ...userOptions && userOptions.testOptions,
|
184 | cwd: projectDir,
|
185 | envName,
|
186 | filename,
|
187 | sourceFileName: path.relative(projectDir, filename),
|
188 | sourceRoot: projectDir
|
189 | });
|
190 |
|
191 | if (!partialTestConfig) {
|
192 | return {hash: '', options: null};
|
193 | }
|
194 |
|
195 | const {options: testOptions} = partialTestConfig;
|
196 | if (!testOptions.plugins.some(containsAsyncGenerators)) {
|
197 | testOptions.plugins.unshift(createConfigItem('@babel/plugin-syntax-async-generators', 'plugin'));
|
198 | }
|
199 |
|
200 | if (!testOptions.plugins.some(containsObjectRestSpread)) {
|
201 | testOptions.plugins.unshift(createConfigItem('@babel/plugin-syntax-object-rest-spread', 'plugin'));
|
202 | }
|
203 |
|
204 | if (!testOptions.plugins.some(containsOptionalCatchBinding)) {
|
205 | testOptions.plugins.unshift(createConfigItem('@babel/plugin-syntax-optional-catch-binding', 'plugin'));
|
206 | }
|
207 |
|
208 | if (ensureStage4 && !testOptions.presets.some(containsStage4)) {
|
209 |
|
210 | testOptions.presets.unshift(createConfigItem('../stage-4', 'preset'));
|
211 | }
|
212 |
|
213 | if (compileEnhancements && !testOptions.presets.some(containsTransformTestFiles)) {
|
214 |
|
215 | testOptions.presets.push(createConfigItem('@ava/babel-preset-transform-test-files', 'preset', {powerAssert: true}));
|
216 | }
|
217 |
|
218 | const hash = hashPartialTestConfig(partialTestConfig, projectDir, pluginAndPresetHashes);
|
219 | const options = babel.loadOptions(testOptions);
|
220 | return {hash, options};
|
221 | };
|
222 |
|
223 | return filename => {
|
224 | const {hash: optionsHash, options} = loadOptions(filename);
|
225 | if (!options) {
|
226 | return null;
|
227 | }
|
228 |
|
229 | const contents = stripBomBuf(fs.readFileSync(filename));
|
230 | const ext = path.extname(filename);
|
231 | const hash = md5Hex([partialCacheKey, contents, optionsHash]);
|
232 | const cachePath = path.join(cacheDir, `${hash}${ext}`);
|
233 |
|
234 | if (fs.existsSync(cachePath)) {
|
235 | return cachePath;
|
236 | }
|
237 |
|
238 | const inputCode = contents.toString('utf8');
|
239 | const inputSourceMap = getSourceMap(filename, inputCode);
|
240 | if (inputSourceMap) {
|
241 | options.inputSourceMap = inputSourceMap;
|
242 | }
|
243 |
|
244 | const {code, map} = babel.transformSync(inputCode, options);
|
245 |
|
246 | if (map) {
|
247 |
|
248 | const mapPath = `${cachePath}.map`;
|
249 | writeFileAtomic.sync(mapPath, JSON.stringify(map));
|
250 |
|
251 |
|
252 |
|
253 | const comment = convertSourceMap.generateMapFileComment(mapPath);
|
254 | writeFileAtomic.sync(cachePath, `${code}\n${comment}`);
|
255 | } else {
|
256 | writeFileAtomic.sync(cachePath, code);
|
257 | }
|
258 |
|
259 | return cachePath;
|
260 | };
|
261 | }
|
262 |
|
263 | module.exports = {
|
264 | validate,
|
265 | build
|
266 | };
|