diff options
Diffstat (limited to 'node_modules/hullabaloo-config-manager/lib/collector.js')
-rw-r--r-- | node_modules/hullabaloo-config-manager/lib/collector.js | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/node_modules/hullabaloo-config-manager/lib/collector.js b/node_modules/hullabaloo-config-manager/lib/collector.js new file mode 100644 index 000000000..1a7414a87 --- /dev/null +++ b/node_modules/hullabaloo-config-manager/lib/collector.js @@ -0,0 +1,332 @@ +'use strict' + +const path = require('path') + +const parseJson5 = require('json5').parse + +const errors = require('./errors') +const readSafe = require('./readSafe') + +function makeValid (source, options) { + // Arrays are never valid options. + if (Array.isArray(options)) throw new errors.InvalidFileError(source) + + // Force options to be an object. Babel itself ignores falsy values when + // resolving config chains. Here such files still need to be included + // for cache busting purposes. + if (!options || typeof options !== 'object') return {} + + return options +} + +function parseFile (source, buffer) { + let options + try { + options = parseJson5(buffer.toString('utf8')) + } catch (err) { + throw new errors.ParseError(source, err) + } + + return makeValid(source, options) +} + +function parsePackage (source, buffer) { + let options + try { + const pkg = JSON.parse(buffer.toString('utf8')) + options = pkg && pkg.babel + } catch (err) { + throw new errors.ParseError(source, err) + } + + return makeValid(source, options) +} + +class Config { + constructor (dir, env, hash, json5, options, source) { + this.dir = dir + this.env = env + this.hash = hash + this.json5 = json5 + this.options = options + this.source = source + + this.babelrcPointer = null + this.envPointers = new Map() + this.extends = null + this.extendsPointer = null + } + + copyWithEnv (env, options) { + return new this.constructor(this.dir, env, this.hash, this.json5, options, this.source) + } + + extend (config) { + const clause = this.takeExtends() + if (clause) { + throw new TypeError(`Cannot extend config: there is an extends clause in the current options: ${clause}`) + } + if (this.extends) { + throw new Error('Cannot extend config: already extended') + } + + this.extends = config + } + + takeEnvs () { + const env = this.options.env + delete this.options.env + + return env + ? new Map( + Object.keys(env) + .filter(Boolean) + .map(name => [name, env[name]])) + : new Map() + } + + takeExtends () { + const clause = this.options.extends + delete this.options.extends + return clause + } +} +exports.Config = Config + +function resolveDirectory (dir, cache) { + const fileSource = path.join(dir, '.babelrc') + const packageSource = path.join(dir, 'package.json') + + const fromFile = readSafe(fileSource, cache) + .then(contents => contents && { + json5: true, + parse () { return parseFile(fileSource, contents) }, + source: fileSource + }) + + const fromPackage = readSafe(packageSource, cache) + .then(contents => contents && { + json5: false, + parse () { return parsePackage(packageSource, contents) }, + source: packageSource + }) + + return fromFile + .then(fileResult => fileResult || fromPackage) + .then(result => { + // .babelrc or package.json files may not exist, and that's OK. + if (!result) return null + + return new Config(dir, null, null, result.json5, result.parse(), result.source) + }) +} + +function resolveFile (source, cache) { + return readSafe(source, cache) + .then(contents => { + // The file *must* exist. Causes a proper error to be propagated to + // where "extends" directives are resolved. + if (!contents) throw new errors.NoSourceFileError(source) + + return new Config(path.dirname(source), null, null, true, parseFile(source, contents), source) + }) +} + +class Chains { + constructor (babelrcDir, defaultChain, envChains) { + this.babelrcDir = babelrcDir + this.defaultChain = defaultChain + this.envChains = envChains + } + + * [Symbol.iterator] () { + yield this.defaultChain + for (const chain of this.envChains.values()) { + yield chain + } + } +} + +class Collector { + constructor (cache) { + this.cache = cache + this.configs = [] + this.envNames = new Set() + this.pointers = new Map() + } + + get initialConfig () { + return this.configs[0] + } + + add (config) { + // Avoid adding duplicate configs. Note that configs that came from an + // "env" directive share their source with their parent config. + if (!config.env && this.pointers.has(config.source)) { + return Promise.resolve(this.pointers.get(config.source)) + } + + const pointer = this.configs.push(config) - 1 + // Make sure not to override the pointer to an environmental + // config's parent. + if (!config.env) this.pointers.set(config.source, pointer) + + const envs = config.takeEnvs() + const extendsClause = config.takeExtends() + const waitFor = [] + + if (config.extends) { + const promise = this.add(config.extends) + .then(extendsPointer => (config.extendsPointer = extendsPointer)) + waitFor.push(promise) + } else if (extendsClause) { + const extendsSource = path.resolve(config.dir, extendsClause) + + if (this.pointers.has(extendsSource)) { + // Point at existing config. + config.extendsPointer = this.pointers.get(extendsSource) + } else { + // Different configs may concurrently resolve the same extends source. + // While only one such resolution is added to the config list, this + // does lead to extra file I/O and parsing. Optimizing this is not + // currently considered worthwhile. + const promise = resolveFile(extendsSource, this.cache) + .then(parentConfig => this.add(parentConfig)) + .then(extendsPointer => (config.extendsPointer = extendsPointer)) + .catch(err => { + if (err.name === 'NoSourceFileError') { + throw new errors.ExtendsError(config.source, extendsClause, err) + } + + throw err + }) + + waitFor.push(promise) + } + } + + for (const pair of envs) { + const name = pair[0] + const options = pair[1] + + this.envNames.add(name) + const promise = this.add(config.copyWithEnv(name, options)) + .then(envPointer => config.envPointers.set(name, envPointer)) + waitFor.push(promise) + } + + return Promise.all(waitFor) + .then(() => pointer) + } + + resolveChains (babelrcDir) { + if (this.configs.length === 0) return null + + // Resolves a config chain, correctly ordering parent configs and recursing + // through environmental configs, while avoiding cycles and repetitions. + const resolveChain = (from, envName) => { + const chain = new Set() + const knownParents = new Set() + + /* eslint-disable no-use-before-define */ + const addWithEnv = config => { + // Avoid unnecessary work in case the `from` list contains configs that + // have already been added through an environmental config's parent. + if (chain.has(config)) return + chain.add(config) + + if (config.envPointers.has(envName)) { + const pointer = config.envPointers.get(envName) + const envConfig = this.configs[pointer] + addAfterParents(envConfig) + } + } + + const addAfterParents = config => { + // Avoid cycles by ignoring those parents that are already being added. + if (knownParents.has(config)) return + knownParents.add(config) + + if (config.babelrcPointer !== null) { + const parent = this.configs[config.babelrcPointer] + addAfterParents(parent) + } + if (config.extendsPointer !== null) { + const parent = this.configs[config.extendsPointer] + addAfterParents(parent) + } + + if (envName) { + addWithEnv(config) + } else { + chain.add(config) + } + } + /* eslint-enable no-use-before-define */ + + for (const config of from) { + if (envName) { + addWithEnv(config) + } else { + addAfterParents(config) + } + } + + return chain + } + + // Start with the first config. This is either the base config provided + // to fromConfig(), or the config derived from .babelrc / package.json + // found in fromDirectory(). + const defaultChain = resolveChain([this.initialConfig]) + + // For each environment, augment the default chain with environmental + // configs. + const envChains = new Map(Array.from(this.envNames, name => { + return [name, resolveChain(defaultChain, name)] + })) + + return new Chains(babelrcDir, defaultChain, envChains) + } +} + +function fromConfig (baseConfig, cache) { + let babelrcConfig = null + for (let config = baseConfig; config; config = config.extends) { + if (config.options.babelrc === false) continue + + if (babelrcConfig) { + throw new TypeError(`${config.source}: Cannot resolve babelrc option, already resolved by ${babelrcConfig.source}`) + } + + babelrcConfig = config + } + + const collector = new Collector(cache) + return Promise.all([ + collector.add(baseConfig), + // Resolve the directory concurrently. Assumes that in the common case, + // the babelrcConfig doesn't extend from a .babelrc file while also leaving + // the babelrc option enabled. Worst case the resolved config is discarded + // as a duplicate. + babelrcConfig && resolveDirectory(babelrcConfig.dir, cache) + .then(parentConfig => { + if (!parentConfig) return + + return collector.add(parentConfig) + .then(babelrcPointer => (babelrcConfig.babelrcPointer = babelrcPointer)) + }) + ]) + .then(() => collector.resolveChains(babelrcConfig && babelrcConfig.dir)) +} +exports.fromConfig = fromConfig + +function fromDirectory (dir, cache) { + dir = path.resolve(dir) + + const collector = new Collector(cache) + return resolveDirectory(dir, cache) + .then(config => config && collector.add(config)) + .then(() => collector.resolveChains(dir)) +} +exports.fromDirectory = fromDirectory |