wallet-core/node_modules/hullabaloo-config-manager/lib/collector.js
2017-05-28 00:40:43 +02:00

333 lines
9.5 KiB
JavaScript

'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