UNPKG

skypager-project

Version:
1,415 lines (1,163 loc) 35.7 kB
/** * @name Project * @description Provides an API for working with a project folder, which is anything that has a package.json in it. * @public * * The Skypager Project is an API for working with the contents, documents, and helpers * inside of a project folder. */ import 'clean-stack' import { resolve, dirname, relative, join } from 'path' import { EventEmitter } from 'fbemitter' import lodash from 'lodash' import findUp from 'find-up' import vm from 'isomorphic-vm' import { mixinPropertyUtils, lazy } from 'skypager-util/lib/properties' import query from 'skypager-util/lib/query' import * as strings from 'skypager-util/lib/string' import Collection from './collection' import Document from './document' import middleware from './middleware' import createDocumentTree from './tree' import Cache from './cache' import { attach as Logger } from './logger' import hashObject from 'object-hash' import Module from 'module' import Helper from './helper' const { isString, flatten, pickBy, castArray, partialRight, uniqueId, mapKeys, omit, isError, attempt, isFunction, defaults, result, mapValues, get, chain } = lodash const { singularize, camelCase, snakeCase } = strings const { keys, assign, defineProperty } = Object export const DefaultOptions = { target: 'node', paths: { source: 'src', cache: 'node_modules/.cache/skypager-project', output: 'dist', public: 'public', library: 'lib', bundles: 'dist/bundles', ast: 'dist/ast', docs: 'docs', data: 'data', logs: 'log', temp: 'tmp', main: 'skypager.js', manifest: 'package.json', } } export const chainSources = [ 'scopes.available', 'constructor.chainProps', 'options.chainProps', ] export const querySources = [ 'scopes.available', 'constructor.queryProps', 'options.queryProps', ] export class Project { /** * Create a Skypager Project wrapper around a package folder. * * @param cwd {String} an absolute path for the Project. Should be the folder which has the package.json in it. * @param options {Object} the options Object * @param options.filesystem {Boolean} use the portfolios file system adapters * @type {Object} */ constructor (cwd, options = {}, context = {}) { mixinPropertyUtils(this) attachEmitter(this) const project = this this.realCwd = cwd this.lazy('argv', () => this.parseArgv()) this.instanceId = result(options, 'id', () => `${this.namespace}-${ Math.floor(Date.now() / 1000)}`) this.hide('current', { collection: 'project', middleware: 'project' }) this.hideGetter('framework', () => context.framework || context.portfolio) this.getter('portfolio', () => context.portfolio) this.hide('manifest', options.manifest || this.readJsonSync(this.join(get(options, 'paths.manifest', 'package.json')))) const computedOptions = defaults(options, this.get('manifest.skypager'), DefaultOptions ) this.hide('options', omit(computedOptions, 'manifest', 'portfolio', 'fs', 'fsx')) this.hide('middleware', middleware(this)) this.hide('config', options.config || {}) this.hide('configurations', {}) this.hide('cache', new Cache()) Logger.call(this, this.getOption('logger', this.getOption('logging')) || {}, {project: this}) require('./middlewares/registries').attach(this, { collections: { type: 'Directory', wrapper: function(cfg, id) { return project.cache.fetch(`collections:${id}:${project.instanceId}:${cfg.cwd}`, () => project.mountCollection(cfg.cwd, cfg)) }, }, middlewares: { type: 'Directory' }, /** project.routes.register('Whatever', { packages: '*:projectType/:projectId/package.json', readmes: ':projectId/README.md', }) project.routes.lookup('whatever').packages */ routes: { type: 'Directory', wrapper: function(fn, id) { const opts = isFunction(fn) ? fn.call(project) : fn return project.cache.fetch(`routes:${id}:${project.cacheKey}`, () => project.createRouteMap(id, opts)) } }, selectors: { type: 'Context', context: require.context('./selectors/project', true, /\.js$/), wrapper: function(fn, id) { return (...args) => project.cache.fetch(`selectors:${id}:${hashObject({args})}:${project.cacheKey}`, () => fn.call(project, project.chain, ...args).value()) } } }) this.register.collection('project', { cwd: this.realCwd, sync: !!options.sync, }) this.register.collection('source', { cwd: this.paths.source, sync: !!options.sync, exclude: ['**/*.spec.*'], }) this.register.collection('specs', { cwd: this.realCwd, sync: false, include: ['**/*.spec.*'], }) this.register.collection('asts', { cwd: this.paths.ast, sync: false, exclude: ['**/*.spec.*'], }) this.register.collection('logs', { cwd: this.paths.logs, sync: false, }) this.register.collection('bundles', { cwd: this.paths.bundles, sync: false, type: 'script', }) this.register.collection('output', { cwd: this.paths.output, sync: false, exclude: ['**/ast/**'], }) if (options.sync) { const cachedCollection = this.collection.cached this.emit('collectionDidLoad', cachedCollection) } this.lazy('files', () => this.collection.files.map((f) => this.loadFile(f))) this.lazy('documents', () => this.collection.withDocuments.documents.map(d => this.loadDocument(d))) this.lazy('exportableDocumentsTree', () => this.createExportableDocumentsTree()) this.lazy('cacheKey', () => this.calculateCacheKey()) this.get('constructor.collectionDelegators', Project.collectionDelegators).forEach(meth => ( this.hideGetter(meth, () => project.result(`collection.${meth}`)) )) this.hide('configHistory', { project: [] }) this.lazy('sandbox', () => this.createSandbox()) this.lazy('vmSandbox', () => this.vm.createContext(this.sandbox)) this.hide('attachedProjectTypes', {}) this.hide('context', () => this.sandbox) } // TODO: When we call project.cwd we should be able to hide the actual path get cwd() { return this.realCwd } get hostname() { try { return require('os').hostname() } catch(error) { return `${this.name}.skypager.local` } } get environment () { return { NODE_ENV: 'development', ...this.getOption('env', process.env), } } get isTest() { return this.argv.test || this.env === 'test' } get isDevelopment() { return !this.isProduction && !this.isTest } get isProduction() { return this.argv.production || this.env === 'production' } get isReact() { return this.hasDependency('react') } get isReactNative() { return this.hasDependency('react-native') } get root() { return this.realCwd } get parentFolder() { return dirname(this.realCwd) } get id() { return `${this.instanceId}-${this.get('manifest.version')}` } get version() { return this.get('options.version') || this.get('manifest.version') || '1.0.0' } get name() { return this.get('options.name') || this.get('manifest.name') || this.constructor.name.toLowerCase() } get paths() { const project = this return { cwd: this.realCwd, ...this.chain .get('options.paths', {}) .defaults(DefaultOptions.paths) .mapValues((p) => (project.resolve(p))) .value() } } /** * A namespace can be set in the Project options, it will default * to the package.json scope if it exists (e.g @skypager/package) otherwise * the name of the Projects parent folder will be used. */ get namespace() { const name = this.name return this.result('options.namespace', () => ( name.match(/^\@\w+\//) ? name.split('/')[0].replace(/\^\@/,'') : name.split('-')[0] ) ) } get packageMain() { return this.resolve(this.get('manifest.main', 'index.js')) } get skypagerMain() { return this.resolve(this.get('manifest.skypager.main', 'skypager.js')) } get runtimeInfo() { return { env: this.env, production: this.isProduction, development: this.isDevelopment, platform: this.platform, hostname: this.hostname, ...(omit(this.argv, '_', '')), } } get platform() { if (this.isBrowser) { return this.isElectronRenderer ? 'electron-renderer' : 'web' } else if (this.isElectron) { return 'electron-main' } else { return 'node' } } get isCli() { return !!this.get('environment.SKYPAGER_CLI', this.get('argv._[1]', '').match(/skypager/)) } get currentCommand() { return this.get('argv._[2]') } get commandPhrase() { return this.get('argv._').slice(2).join(" ") } get isServer() { return !!this.get('environment.SKYPAGER_SERVER', this.currentCommand === 'start') } get isElectron() { return this.get('environment.SKYPAGER_ELECTRON') || `${process.argv.join(' ')}`.match(/\/Electron/) } get isElectronRenderer() { return !this.isNode && isElectronRenderer() } get isBrowser() { return typeof document !== 'undefined' && typeof window !== 'undefined' && typeof document.getElementById !== 'undefined' } get isNode() { return !this.isBrowser && process & !process.type } get isWindows() { return /^win/.test(process.platform); } get isMacOS() { return /^darwin/.test(process.platform); } get isLinux() { return /^linux/.test(process.platform); } get helpers() { return Helper.registry } get projectTypeConfig() { return this.get('options.projectTypes', {}) } get featureConfig() { return this.get('options.features', {}) } get availableProjectTypes() { return this.get('projectTypes.available', []) } get availableFeatures() { return this.get('features.available', []) } get allHelpers() { return Helper.allHelpers } get availableHelpers() { return Helper.available } get projectTypeIds() { return this.chain .get('projectTypeConfig') .pick(...this.availableProjectTypes) .keys() .value() } get featureIds() { return this.chain .get('featureConfig') .pick(...this.availableFeatures) .keys() .value() } get projectTypeHelpers() { return this.chain .get('projectTypeIds') .keyBy(v => v) .mapValues(v => this.projectType(v, this.get(`projectTypeConfig.${v}`, {}))) .values() .value() } get featureHelpers() { return this.chain .get('featureIds') .keyBy(v => v) .mapValues(v => this.feature(v, ({ cacheHelper: true, ...this.get(['featureConfig', v], {}), }))) .values() .value() } attachProjectTypes(types = this.projectTypeIds) { return this.projectTypeHelpers .reduce((memo, projectType) => { projectType.attach() return memo }, this) } enableFeatures() { return this.featureHelpers .reduce((memo, feature) => { feature.enable() return memo }, this) } hashObject(...args) { return hashObject(...args) } /** * Access one of the project options, providing a default value should one * not already be set. * @param {String} value an object path including something like whatever.nested.value * @param {Any} defaultValue an optional default value should the requested value not be defined */ getOption(key, defaultValue) { key = ['options', ...flatten(castArray(key).map(k => k.split('.')))] return this.result(key, this.getConfig(key, defaultValue)) } resultOption(key, defaultValue) { key = ['options', ...flatten(castArray(key).map(k => k.split('.')))] return this.result(key, this.getConfig(key, defaultValue)) } getConfig(key, defaultValue) { key = ['config', ...flatten(castArray(key).map(k => k.split('.')))] return this.result(key, defaultValue) } get documentClass() { return this.getOption('documentClass', Document) } get documentSelectors() { return this.documentClass.selectors || Document.selectors } get documentTransforms() { return this .get('documentTypes.available', []) .map(this.documentType.bind(this)) .filter(docType => docType.getTransforms ) .reduce((memo, docType) => ({ ...memo, ...docType.getTransforms() }), []) } get documentUtils() { return this .get('documentTypes.available', []) .map(this.documentType.bind(this)) .filter(docType => docType.getTransforms ) .reduce((memo, docType) => ({ ...memo, ...docType.getTransforms() }), []) } get lookup() { const p = this return p.registries.available .reduce((memo, prop) => ({ ...memo, [singularize(prop)](...args) { return p.registries.lookup(prop).lookup(...args) } }), {}) } get register() { const p = this return p.registries.available .reduce((memo, prop) => ({ ...memo, [singularize(prop)](...args) { return p.registries.lookup(prop).register(...args) } }), {}) } hasDependency(dep) { return typeof this.select('dependencies')[dep] !== 'undefined' } document (documentId) { const doc = this.get(['documentsMappedById', documentId]) return doc ? doc.ready : doc } get documentsTree() { return createDocumentTree(this, { ready: false, }) } get loadedDocumentsTree() { return createDocumentTree(this, { ready: true, }) } createExportableDocumentsTree() { return mapValues(this.loadedDocumentsTree, (v) => ( v.createExportable ? v.exportables : v )) } get cachePath() { return this.resolve(this.paths.cache, this.cacheKey) } clearFileCache() { return this.fsx.removeAsync(this.cachePath) } createFileCache(location = this.cachePath) { return this.mkdirpAsync(location) } readJsonCache(relativePath, ...args) { return this.readJsonAsync( this.resolve(this.cachePath, relativePath), ...args ) } readCacheFile(relativePath, ...args) { return this.readFileAsync( this.resolve(this.cachePath, relativePath), ...args ) } cacheJson(relativePath, object) { return this.createFileCache() .then(() => this.writeJsonAsync(this.resolve(this.cachePath, relativePath), object, 'utf8')) } cacheFile(relativePath, contents) { return this.createFileCache() .then(() => this.writeFileAsync(this.resolve(this.cachePath, relativePath), contents, 'utf8')) } get chain() { return lodash.chain(this) } get collection() { return this.collections.lookup(this.current.collection) } c(collectionId = this.current.collection) { return this.collections.lookup(collectionId) } /** * Query the project documents using a parameters hash, * a route pattern, or a filter function. Will return an * array of matching documents that are ready to work with. * * @param {String,Object,Function} params - your query parameters * @param {Boolean} chain - whether to keep the results wrapped in the lodash chain, defaults to true. */ query(params, stayChained = true, getDoc) { const meth = stayChained ? this.selectChain : this.select getDoc = isFunction(getDoc) ? getDoc.bind(this) : (result) => result.doc.ready const safe = function(result) { try { return getDoc(result) } catch(error) { return undefined } } return this.cache.fetch(`queries:${this.cacheKey}:${hashObject({params, getDoc: getDoc.toString(), stayChained})}`, () => { return meth.call(this, (c) => ( typeof params === 'string' ? c.invoke('route', params).map(safe).compact() : c.invoke('queries.documents', params).map(safe).compact() )) }) } /** * Finds all documents matching a given route pattern. * Route patterns refer to express / rails style route strings * and are compiled using the path-to-regexp library. * * @example * * project.route('src/helpers/:helperType/:helperCategory/:helperName.js') * * This will return all files such as "src/helpers/whatever/category/name.js" * along with a metadata object {helperType:"whatever", helperCategory: "category", helperName: "name"} * * These attributes can be combined with */ route(pattern, stayChained = false, processResult = mirrorFn) { if(this.routes.available.indexOf(pattern) >= 0) { return this.invoke(stayChained ? 'selectChain' : 'select', (c) => c.invoke('routes.lookup', pattern)) } return this.routeDocuments(pattern, stayChained, processResult) } routeDocuments(pattern, stayChained = false, processResult = mirrorFn) { const project = this const collection = this.collection const formatKeys = (obj) => mapKeys(obj, (v,k) => { return k.toString().length === 1 ? `key${k}` : k }) const result = this.chain.get('collection.routers') .invoke('documents', pattern) .map(result => { const id = result.path[0] const doc = result.path[1] const file = doc.file const idParts = id.split('/') const meta = formatKeys(get(result, 'result', {})) return assign({}, meta, { id, doc, idParts, baseFolder: idParts[0], categoryFolder: idParts[1] || idParts[0], meta, file: assign({ path: file.path, }, project.wrapFile(file)), }) }) .map(result => processResult(result)) return stayChained ? result : result.value() } routeFiles(pattern, chain) { const project = this const collection = this.collection const results = collection.routers.files(pattern).map(result => { const file = result.path[1] const id = file.relative const idParts = id.split('/') return { id, file, idParts, baseFolder: idParts[0], categoryFolder: idParts[1] || idParts[0], meta: get(result, 'result', {}), ...(project.wrapFile(file)) } }) return chain ? lodash.chain(results) : results } get vm() { return vm } get manifestModuleId() { return __non_webpack_require__.resolve(this.paths.manifest) } get module() { const result = require.cache[this.manifestModuleId] if (!result) { __non_webpack_require__(this.paths.manifest) } return require.cache[this.manifestModuleId] } /** * Takes a map of named routes and returns a function interface for each one */ createRouteMap(name, cfg = {}, defaultOptions = {}) { const functions = mapValues(cfg, (options, key) => { if (typeof options === 'string') { options = { patterns: [options.toString()] } } const cacheKey = [ `routes/${name}/${key}`, options.patterns.join('|') ].join(':') return () => this.cache.fetch(cacheKey, this.createRouteFunction({ ...defaultOptions, ...options, })) }) const mapper = {} keys(functions) .forEach(key => defineProperty(mapper, key, { enumerable: true, configurable: false, get: functions[key], })) return mapper } createRouteFunction(options = {}) { if (typeof options === 'string') { options = { patterns: [options.toString()] } } let { baseRoute = '', patterns } = options patterns = castArray(patterns).map(pattern => `${baseRoute}${pattern}`) return () => ( lodash.chain(patterns) .map((pattern) => this.route(pattern)) .flatten() .compact() .uniqBy(r => r.id) .map((result, index) => isFunction(options.transform) ? options.transform.call(this, result, index) : result ) .value() ) } invoke(...args) { return this.chain.invoke(...args).value() } run(helperId, options = {}, callback) { const [type, id] = helperId.split('/') if (isFunction(options)) { callback = options options = {} } const helper = this.invoke(type.replace(/s$/,''), id, options) return Promise.resolve(helper.run()) .catch((error) => isFunction(callback) && callback(null, error)) .then(result => { isFunction(callback) && callback.call(this, result) return result }) } resolvePromiseHash(object = {}) { return promiseHash(object) } selectAsync(selector, ...args) { return this.selectChain(selector, ...args) .thru((result) => this.resolvePromiseHash(result)) .value() } createSandbox (locals = {}) { return { project: this, Skypager: this.portfolio, lodash, console, ...locals, } } loadFile (file) { return file } loadDocument (doc) { return doc } visit(route, visitor) { this.invoke('route', route).forEach( (result, _index) => result.doc.visit( partialRight(visitor, {...result, _index }, this.createSandbox())) ) } createSandboxedScriptRunner(code, options = {}) { const script = this.createScript(code, omit(options, 'sandbox')) return () => ( options.sync ? script.runInContext(options.sandbox || this.vmSandbox) : Promise.resolve(script.runInContext(options.sandbox || this.vmSandbox)) ) } createScriptRunner(code, options = {}, context = {}) { const script = this.createScript(code, options) return (ctx = {}) => script.runInContext(options.sandbox || this.vm.createContext({ ...this.sandbox, ...options, ...context, ...ctx, })) } createModule(code = '', options = {}) { const project = this const runCode = Module.wrap(code) options = { filename: `${this.name}-module-${uniqueId()}.js`, dirname: this.realCwd, ...options, } const id = require('path').join(options.dirname, options.filename) const mod = new Module(id) mod.paths = Module._nodeModulePaths(options.dirname) if (options.moduleFolders) { mod.paths.push(...options.moduleFolders) } mod.filename = options.filename return { code: code, id, mod, options, get ctx() { return options.sandbox || project.vm.createContext({ ...project.sandbox, ...options.context || {}, module: mod, exports: mod.exports, require: mod.require, }) }, get script() { return project.vm.createScript(Module.wrap(code), options) }, get exports() { this.script.runInContext(this.ctx)( mod.exports, mod.require, mod, options.filename, options.dirname ) mod.loaded = true return mod.exports } } } createScript(code = '', options = {}) { return new this.vm.Script(code.toString(), options) } get babelRc() { const path = this.findUp('.babelrc', {cwd: this.join('whatever')}) return path && this.readJsonSync(path) } createAsyncScriptRunner(code = '', options = {}, context = {}) { return (ctx = {}) => ( Promise.resolve() .then(() => this.createScriptRunner(code, options, context)(ctx)) ) } wrapFile (file) { const project = this const { path } = file return { contents() { return project.readFileSync(path).toString() }, getFile() { return file }, json() { return project.readJsonSync(path) }, read() { return project.readFileAsync(path).then(file => file.toString()) }, readJson() { return project.readJsonAsync(path) }, } } /** * Get information about this project's parent. It will look up the path for the closest package.json * @return {Object} An object which provides access to the package.json path, folder path, and contents */ get parentModule() { const proj = this return { get path() { return proj.findUp('package.json', { cwd: proj.parentFolder, }) }, get folder() { return dirname(proj.parentModule.path) }, get manifest() { try { return proj.readJsonSync( proj.parentModule.path ) } catch(e) { return proj.manifest } }, get projectLoader() { return (options = {}) => proj.portfolio.load(proj.parentModule.folder, options) }, get project() { return proj.parentModule.projectLoader({ sync: false, fresh: true, }) } } } parseArgv() { const base = defaults({}, this.getOption('argv'), require('minimist')(process.argv)) const transformedKeys = mapKeys(omit(base, '_'), (v,k) => camelCase(snakeCase(k))) return assign(base, transformedKeys) } get env() { const env = this.environment return env.SKYPAGER_ENV ? env.SKYPAGER_ENV : env.NODE_ENV } calculateCacheKey() { try { return this.unsafeCalculateCacheKey() } catch(error) { return this.id } } // this should be the same every time the project fires up, unless any files have changed unsafeCalculateCacheKey() { const wrappers = this.chain.get('collection.importer.fileWrappersSync') this.maxUpdatedAt = wrappers.map(f => ( f.stat && f.stat.mtime && f.stat.mtime.getTime() )).max().value() this.filesCount = wrappers.size().value() const { branch } = this.gitInfo return [ this.name, this.version, branch, this.maxUpdatedAt, this.filesCount, ].join(':') } get sourceFilePaths() { const sourcePaths = this.validSourcePaths return this.files .filter(f => sourcePaths.indexOf(f.dirname) >= 0).map(f => f.path) .filter(f => !f.match(/(spec|test).*\.\w+$/)) } get validSourcePaths() { return this.sourcePaths.filter(p => this.fs.existsSync(p)) } get sourcePaths() { return this .chain.get('options.sourcePaths', []) .concat([this.paths.source]) .uniq() .value() } get chains() { const project = this const chains = this.chain .at(...chainSources) .flatten() .compact() .uniq() .reduce((memo, prop) => { lazy(memo, prop, ()=> chain(project.get(prop))) return memo },{}) return chains.value() } get queries() { const project = this const queries = this.chain .at(...querySources) .flatten() .compact() .uniq() .reduce((memo, prop) => { memo[prop] = (...args) => query(project.get(prop), ...args) return memo },{}) return queries.value() } get gitignores() { return this.getOption('ignorePaths', ['tmp/', 'log/', 'dist/', '.DS_Store']) .concat(this.gitignoreContents) .map(p => `!${p}`) } get gitignoreContents() { return this.attemptReadSync('.gitignore', '').split("\n") .filter(p => p && p.length > 2 && !p.trim().startsWith('#')) } get skypagerIgnoreContents() { return this.attemptReadSync('.skypagerignore', '').split("\n") .filter(p => p && p.length > 2 && !p.trim().startsWith('#')) } /** PATH HELPERS Helpers to make working with project paths easier */ join(...args) { return join(this.realCwd, ...args) } relative(...args) { return relative(this.realCwd, ...args) } resolve(...args) { return resolve(this.realCwd, ...args) } attempt(...args) { return attempt(...args) } attemptReadSync(path, fallbackValue = '') { const result = attempt( () => this.fs.readFileSync(this.join(path)).toString() ) return isError(result) ? fallbackValue : result } use (middleware) { if (isFunction(middleware.preRegister)) { middleware.preRegister.call(this, this) } if (isString(middleware) && this.availableProjectTypes.indexOf(middleware) !== -1) { project.projectType(middleware).use() } else if (isFunction(middleware) && middleware.providesProjectType) { this.middleware.use(middleware.bind(this)) } else if (isFunction(middleware)) { this.middleware.use(middleware.bind(this)) } return this } get autostart() { return this.startAsync().then(() => { return this }) } start (options = {}, callback) { const project = this if (this.started) { if (isFunction(callback)) { callback.call(project, null, project) return this } else { return Promise.resolve(this) } } if (isFunction(callback)) { return this.middleware.run(options, this.sandbox, (...args) => { if(!args[0]) { this.started = true } callback.call(project, ...args) }) } else { return this.startAsync(options, this.sandbox) .then((...args) => callback.call(project, ...args) ) } } startAsync (options = {}) { if (this.started) { return Promise.resolve(this) } return new Promise((resolve,reject) => { try { this.middleware.run(options, this.sandbox, (err, ...callbackArgs) => { if (err) { reject(err) } else { resolve(this) } }) } catch(error) { throw(error) } }) .then(() => { this.started = true return this }) .catch(error => { console.log('Error in start async method', error) }) } mixin (methods = {}, target = this) { methods = pickBy(methods, (v) => typeof v === 'function') return assign(target, mapValues(methods, (fn) => partialRight(fn.bind(target), target.sandbox || target.context))) } mountCollection (cwd = this.realCwd, options = {}) { const project = this options = { importerOptions: project.get('options.importer', project.get('options.importerOptions', {})), project: project, documentClass: Document, ...options, exclude: (options.exclude === false || options.includeAll ? [] : [ ...options.exclude || [], ...options.ignore || [], ...(project.gitignores || []).filter(f => !f.startsWith('!')), ...project.skypagerIgnoreContents || [], '**/log/**', '**/bin/**', ]), } return Collection.mount(cwd, { ...options, collection: this, }) } findUp(fileName, options = {}) { return findUp.sync(fileName, { cwd: this.parentFolder, ...options, }) } select(selector, ...args) { if (isFunction(selector)) { const result = selector.call(this, this.chain, ...args) return result && typeof result.value === 'function' ? result.value() : result } else if (typeof selector === 'string') { return this.selectors.lookup(selector)(...args) } } selectChain(selector, ...args) { return this.chain.invoke('select', selector, ...args) } selectorMap (config = {}, resolveChain = true) { const c = this.chain .plant(config) .mapValues((selector) => this.select(...castArray(selector))) return resolveChain ? c.value() : c } createEntityFrom(...properties) { const src = this.slice(...properties) return require('./entity').createEntity(src) } /** * Create a lodash chain by picking some properties from the project. * @param {Array[String]} properties List of properties you want to be a part of the chain * @return {Chain} The lodash chain whose initial value consists of the properties given */ slice (...properties) { return properties.reduce((memo, prop) => { lodash.set(memo, prop, this.result(prop)) return memo }, {}) } /** * Create a lodash chain with the given object paths. */ at (...paths) { return this.chain.at(...paths) } getEither(attempts = [], defaultValue) { const match = attempts.find(property => this.has(property)) return match ? this.get(match, defaultValue) : defaultValue } resultEither(attempts = [], defaultValue) { const match = attempts.find(property => this.has(property)) return match ? this.result(match, defaultValue) : defaultValue } static collectionDelegators = [ 'childFolderNames', 'childFolderPaths', 'subFolderNames', 'subFolderPaths', 'rootFiles', 'groupedByChildFolder', 'groupedByChildFolderAndExtension', 'relativeFilePaths', 'relativeSubFolderPaths', 'getFileByRelativePath', 'filesIndex', 'relativeObjectPaths', 'subFolderObjectPaths', 'routers', 'documentsMap', 'documentsMappedById', 'documentEntryPoints', 'depths', 'gitInfo', ] static chainProps = [ 'files', 'documents', 'childFolderPaths', 'childFolderNames', 'subFolderNames', 'subFolderPaths', ] static queryProps = [ 'files', 'documents', 'childFolderPaths', 'childFolderNames', 'subFolderNames', 'subFolderPaths', ] static get Collection() { return Collection } static get Document() { return Document } static load (cwd, options = {}, context = {}) { return new this(cwd, options, context) } static loadSync(cwd, options = {}, context = {}) { return this.load(cwd, { ...options, sync: true }, context) } } export default Project export function load(cwd = process.cwd(), options = {}, context = {}) { return new Project(cwd, options, context) } export function attachEmitter(project) { project.hide('emitter', new EventEmitter()) project.hide('on', project.emitter.addListener.bind(project.emitter)) project.hide('once', project.emitter.once.bind(project.emitter)) project.hide('off', project.emitter.removeCurrentListener.bind(project.emitter)) project.hide('emit', project.emitter.emit.bind(project.emitter)) project.hide('trigger', project.emitter.emit.bind(project.emitter)) } export function attachRegistry (project, name, options = {}) { if (project.registries.available.indexOf(name) >= 0) { return project.registries.lookup(name) } project.registries.register(name, () => create({ name, type: 'Directory', ...options, })) if (options.prop !== false && typeof project[name] === 'undefined') { project.getter(name, () => project.registries.lookup(name)) } if (options.prop !== false && typeof project[singularize(name)] === 'undefined') { project.getter(singularize(name), (...args) => project[name].lookup(...args)) } return project } function mirrorFn(obj) { return obj } function isElectronRenderer () { // node-integration is disabled if (!process) return true // We're in node.js somehow if (!process.type) return false return process.type === 'renderer' } export function promiseHash(obj) { var awaitables = [] var keys = Object.keys(obj) for (var i = 0; i < keys.length; i++) { var key = keys[i] var a$ = obj[key] awaitables.push(a$) } return Promise.all(awaitables).then(function (results = {}) { var byName = {} for (var i = 0; i < keys.length; i++) { var key = keys[i] byName[key] = results[i] } return byName }) }