From 49a60f155b64e00a5f3e879540ce4be7bbb74691 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 20 May 2016 12:11:43 -0400 Subject: [PATCH] Support .babelrc presets and plugins. In addition to package.json files with "babel" sections, BabelCompiler now supports .babelrc files, though in both cases only the "presets" and "plugins" fields are respected. If a .babelrc file is found, package.json files are ignored. Additional presets and plugins are now *prepended* to the original babelOptions.{presets,plugins} lists, so that the custom plugins have a chance to handle syntax differently than babel-preset-meteor would. The inputFile.getPackageJson method has been replaced by a more general method, inputFile.findControlFile. Fixes #6351. --- packages/babel-compiler/babel-compiler.js | 85 ++++++++++++++++++----- tools/isobuild/compiler-plugin.js | 40 +++++------ 2 files changed, 85 insertions(+), 40 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 169b727b56..c1f84f5456 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -5,10 +5,13 @@ */ BabelCompiler = function BabelCompiler(extraFeatures) { this.extraFeatures = extraFeatures; + this._babelrcCache = Object.create(null); }; var BCp = BabelCompiler.prototype; var excludedFileExtensionPattern = /\.es5\.js$/i; +var fs = Npm.require("fs"); +var hasOwn = Object.prototype.hasOwnProperty; var strictModulesPluginFactory = Npm.require("babel-plugin-transform-es2015-modules-commonjs"); @@ -84,7 +87,7 @@ BCp.processFilesForTarget = function (inputFiles) { babelOptions.plugins.push(babelModulesPlugin); } - inferExtraBabelOptions(inputFile, babelOptions); + self.inferExtraBabelOptions(inputFile, babelOptions); babelOptions.sourceMap = true; babelOptions.filename = @@ -133,28 +136,64 @@ function profile(name, func) { } }; -function inferExtraBabelOptions(inputFile, babelOptions) { - const pkgJson = - inputFile.require && - inputFile.getPathInPackage && - inputFile.getPackageJson(); +BCp.inferExtraBabelOptions = function (inputFile, babelOptions) { + if (! inputFile.require || + ! inputFile.findControlFile) { + return false; + } - if (! pkgJson || ! pkgJson.babel) { - return; + return ( + // If a .babelrc exists, it takes precedence over package.json. + this._inferFromBabelRc(inputFile, babelOptions) || + this._inferFromPackageJson(inputFile, babelOptions) + ); +}; + +BCp._inferFromBabelRc = function (inputFile, babelOptions) { + var babelrcPath = inputFile.findControlFile(".babelrc"); + if (babelrcPath) { + if (! hasOwn.call(this._babelrcCache, babelrcPath)) { + this._babelrcCache[babelrcPath] = + JSON.parse(fs.readFileSync(babelrcPath)); + } + + return this._inferHelper( + inputFile, + babelOptions, + this._babelrcCache[babelrcPath] + ); + } +}; + +BCp._inferFromPackageJson = function (inputFile, babelOptions) { + var pkgJsonPath = inputFile.findControlFile(".babelrc"); + if (pkgJsonPath) { + if (! hasOwn.call(this._babelrcCache, pkgJsonPath)) { + this._babelrcCache[pkgJsonPath] = + JSON.parse(fs.readFileSync(pkgJsonPath)).babel || null; + } + + return this._inferHelper( + inputFile, + babelOptions, + this._babelrcCache[pkgJsonPath] + ); + } +}; + +BCp._inferHelper = function (inputFile, babelOptions, babelrc) { + if (! babelrc) { + return false; } function infer(listName, prefix) { - const list = pkgJson.babel[listName]; - if (! Array.isArray(list)) { + var list = babelrc[listName]; + if (! Array.isArray(list) || list.length === 0) { return; } - function addPrefix(id) { - return isTopLevel ? prefix + id : id; - } - function req(id) { - const isTopLevel = "./".indexOf(id.charAt(0)) < 0; + var isTopLevel = "./".indexOf(id.charAt(0)) < 0; if (isTopLevel) { // If the identifier is top-level, it will be prefixed with // "babel-plugin-" or "babel-preset-". If the identifier is not @@ -166,17 +205,27 @@ function inferExtraBabelOptions(inputFile, babelOptions) { return inputFile.require(id); } - list.forEach(function (item) { + list.forEach(function (item, i) { if (typeof item === "string") { item = req(item); } else if (Array.isArray(item) && typeof item[0] === "string") { + item = item.slice(); // defensive copy item[0] = req(item[0]); } - babelOptions[listName].push(item); + list[i] = item; }); + + // PREPEND additional plugins to the existing babelOptions[listName] + // list, so that they have a chance to handle syntax differently than + // babel-preset-meteor normally would. + var target = babelOptions[listName] || []; + target.unshift.apply(target, list); + babelOptions[listName] = target; } infer("presets", "babel-preset-"); infer("plugins", "babel-plugin-"); -} + + return true; +}; diff --git a/tools/isobuild/compiler-plugin.js b/tools/isobuild/compiler-plugin.js index 9d16f84072..9cb974611c 100644 --- a/tools/isobuild/compiler-plugin.js +++ b/tools/isobuild/compiler-plugin.js @@ -204,10 +204,9 @@ class InputFile extends buildPluginModule.InputFile { // document. this._resourceSlot = resourceSlot; - // This `false` means we haven't read the package.json file governing - // this InputFile yet. Once we read it, this cached value will be - // either an object or null (meaning there was no package.json file). - this._packageJson = false; + // Map from control file names (e.g. package.json, .babelrc) to + // absolute paths, or null to indicate absence. + this._controlFileCache = Object.create(null); // Map from imported module identifier strings (possibly relative) to // fully require.resolve'd module identifiers. @@ -257,35 +256,32 @@ class InputFile extends buildPluginModule.InputFile { return self._resourceSlot.inputResource.fileOptions || {}; } - getPackageJson() { - if (typeof this._packageJson === "object") { - // Note that this._packageJson could be either an actual object or - // null at this point, which may be the first time I've ever been - // glad that typeof null === "object". - return this._packageJson; + // Search ancestor directories for control files (e.g. package.json, + // .babelrc), and return the absolute path of the first one found, or + // null if the search failed. + findControlFile(basename) { + let absPath = this._controlFileCache[basename]; + if (typeof absPath === "string") { + return absPath; } const sourceRoot = this._resourceSlot.packageSourceBatch.sourceRoot; if (! _.isString(sourceRoot)) { - return this._packageJson = null; + return this._controlFileCache[basename] = null; } let dir = files.pathDirname(this.getPathInPackage()); while (true) { - const pkgJsonId = files.convertToPosixPath( - files.pathJoin(sourceRoot, dir, "package.json")); + absPath = files.pathJoin(sourceRoot, dir, basename); - try { - // The require function will cache results across the process. - return this._packageJson = require(pkgJsonId); - } catch (e) { - if (e.code !== "MODULE_NOT_FOUND") { - throw e; - } + const stat = files.statOrNull(absPath); + if (stat && stat.isFile()) { + return this._controlFileCache[basename] = absPath; } if (files.pathBasename(dir) === "node_modules") { - return this._packageJson = null; + // The search for control files should not escape node_modules. + return this._controlFileCache[basename] = null; } let parentDir = files.pathDirname(dir); @@ -293,7 +289,7 @@ class InputFile extends buildPluginModule.InputFile { dir = parentDir; } - return this._packageJson = null; + return this._controlFileCache[basename] = null; } resolve(id) {