aboutsummaryrefslogtreecommitdiff
path: root/node_modules/hullabaloo-config-manager/lib/collector.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/hullabaloo-config-manager/lib/collector.js')
-rw-r--r--node_modules/hullabaloo-config-manager/lib/collector.js332
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