skypager-project
Version:
skypager project framework
1,415 lines (1,163 loc) • 35.7 kB
JavaScript
/**
* @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
})
}