UNPKG

module-composer

Version:

Bring order to chaos. Level up your JS application architecture with Module Composer, a tiny but powerful module composition utility based on functional dependency injection.

77 lines (59 loc) 2.84 kB
const _ = require('./util'); module.exports = session => (path, deps, opts = {}) => { if (!path) throw new Error('Missing path'); if (!_.has(session.target, path)) throw new Error(`${path} not found`); const target = _.get(session.target, path); if (!_.isPlainObject(target)) throw new Error(`${path} must be a plain object`); const key = path.split('.').pop(); if (deps?.[key]) throw new Error(`${key} already exists`); if (session.dependencies[key]) throw new Error(`${key} is already composed`); const options = session.getComposeOptions(key, opts); const { args, customiser, depth, flat, overrides } = options; if (depth === 0 && deps) throw new Error('Unexpected deps'); const self = {}; const selfDeps = { ...session.configAliases, self, [key]: self, ...deps }; const recurse = (target, deps = {}, currentDepth = 0, here = undefined, parent = undefined) => { if (currentDepth === depth) return target; if (!_.isPlainObject(target)) return target; const isTopLevel = currentDepth === 0; here = here || (isTopLevel ? self : {}); // Phase 1: shallow assign all keys (without evaluating functions) for (const [key, val] of Object.entries(target)) { here[key] = _.isPlainObject(val) ? recurse(val, deps, currentDepth + 1, undefined, here) : val; } // Phase 2: now evaluate any functions in-place for (const [key, val] of Object.entries(here)) { if (_.isPlainFunction(val)) { const result = val({ self, here, parent, ...deps }, args); // only evaluate top-level function (not returned functions) here[key] = result; } } return (flat && isTopLevel) ? _.flattenObject(here) : here; }; const apply = fns => input => fns.reduce((acc, fn) => Object.assign(acc, fn(acc)), input); const precomposers = apply([ ...session.precomposers, ({ target, deps }) => ({ target: recurse(target, { ...selfDeps, ...deps }) }), ({ target }) => ({ target: _.invokeAtOrReturn(target, customiser, args) }) ]); const postcomposers = apply([ ({ target }) => ({ target: _.merge(target, overrides) }), ...session.postcomposers, ({ key, target }) => { session.registerModule({ path, key, target, deps, options }); return {}; } ]); const initial = { key, target, deps, self, options }; const { target: maybePromise, ...rest } = precomposers(initial); const next = final => { postcomposers({ ...rest, target: final }); return session.modules; }; return _.isPromise(maybePromise) ? maybePromise.then(next) : next(maybePromise); };