skypager-project
Version:
skypager project framework
342 lines (264 loc) • 7.67 kB
JavaScript
import findUp from 'findup-sync'
import get from 'lodash/get'
import mapKeys from 'lodash/mapKeys'
import defaults from 'lodash/defaults'
import isFunction from 'lodash/isFunction'
import compact from 'lodash/compact'
import uniq from 'lodash/uniq'
import isError from 'lodash/isError'
import { promisify } from 'bluebird'
import { join, basename } from 'path'
import { mixinPropertyUtils } from 'skypager-util'
import config from './config/index'
import filesystem from './file-systems/node'
import Helper from './helper'
import Project from './project'
import Cache from './cache'
import minimist from 'minimist'
const InstanceIdMap = {}
const ProjectsCache = {}
const { keys, getOwnPropertyDescriptors } = Object
const ARGV = minimist(process.argv)
export class Portfolio {
static get Project() {
return Project
}
static get Helper() {
return Helper
}
get promisify() {
return promisify
}
constructor (name = 'Skypager', options = {}) {
if (typeof name === 'object') {
options = name
name = options.name || this.constructor.name
}
mixinPropertyUtils(this)
this.hide('configuration', {})
this.instanceId = `${name}-${ Math.floor(Date.now() / 1000)}`
this.hide('cache', new Cache())
this.hide('__project_cache', ProjectsCache[this.instanceId] = {})
this.hide('moduleId', get(module, 'id', __filename))
filesystem({
host: this
})
if (!this.dirname) {
this.dirname = join(__dirname, '..')
}
}
get helpers() {
return Helper.registry
}
get Project() {
return this.get('options.Project', Project)
}
get Helper() {
return this.get('options.Helper', Helper)
}
// TODO
// I want to try and use the trick i learned from terse-webpack which uses
// a registry of features and reducers to expose a composable / chainable
// configuration functions that can be used to create presets as project templates
configure (scope = 'portfolio', options = {}) {
const current = this.configuration[scope]
if (current) {
return config({
...options,
history: current.history,
})
}
return this.configuration[scope] = config({
...options,
history: [{scope}]
})
}
createProject(cwd, options = {}) {
if (typeof cwd === 'undefined') {
throw new Error('Must pass a Skypager a directory to load')
}
defaults(options, {
manifestFilename: 'package.json',
manifestPath: join(cwd, 'package.json'),
type: 'default',
membershipKey: 'skypager',
})
if (!this.fsx.existsSync(options.manifestPath)) {
throw('Can not load a Skypager project without a manifest file')
}
const manifest = this.fsx.readJsonSync(options.manifestPath)
const manifestOptions = get(manifest, options.membershipKey, {})
const ProjectType = this.Project
return ProjectType.load(cwd, {
manifest,
...manifestOptions,
...options,
}, {
portfolio: this,
framework: this.framework,
})
}
/**
* Load a project from a folder, checking the cache first to see if it exists.
*/
load (cwd, options = {}, context = {}) {
if (typeof cwd === 'undefined') {
throw new Error('Must pass a Skypager a directory to load')
}
const instanceId = get(this.__project_cache, cwd)
if (!options.fresh && instanceId && InstanceIdMap[instanceId]) {
return InstanceIdMap[instanceId]
}
const project = this.createProject(cwd, options, {
portfolio: this,
framework: this.framework,
...context,
})
filesystem({
host: project
})
this.__project_cache[cwd] = project.id
Helper.attachAll(project)
const passedThrough = InstanceIdMap[project.id] = this.projectDidLoad
? passThroughLocalConfig(this.projectDidLoad(project))
: passThroughLocalConfig(project)
return passedThrough
}
get framework() {
return this.constructor
}
get allHelpers() {
return Helper.allHelpers
}
get allProjects() {
return this.projectCacheKeys
.map(i => this.__project_cache[i])
.map(instanceId => InstanceIdMap[instanceId])
}
get projectInstanceIds() {
return keys(InstanceIdMap)
}
get projects() {
return mapKeys(this.__project_cache, (project) => project.name)
}
get projectCacheKeys() {
return keys(getOwnPropertyDescriptors(this.__project_cache))
}
get projectRoots() {
return this.allProjects.map(
p => p.cwd
)
}
get requireCacheEntries () {
const moduleIds = keys(require.cache)
return this.projectRoots.map(rootPath => (
moduleIds.filter(moduleId => moduleId.startsWith(rootPath))
))
}
get userHome() {
return (process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME']) || process.cwd()
}
get isDevelopingLocally() {
return this.dirname === process.cwd()
}
get isNodeModule() {
return this.parentFolderName === 'node_modules'
}
get localPackage() {
return findUp('package.json', {
cwd: process.cwd()
})
}
get parentPackage() {
return this.parentPackagePath && this.fsx.readJsonSync(this.parentPackagePath)
}
get parentPackagePath() {
return findUp('package.json', {
cwd: this.parentFolder
})
}
get parentFolder () {
return join(this.dirname, '..')
}
get parentFolderName() {
return basename(this.parentFolder)
}
get grandParentFolder() {
return join(this.parentFolder, '..')
}
clearProjectCache() {
this.requireCacheEntries.forEach(entry => delete(require.cache[entry]))
this.projectCacheKeys.forEach(cacheKey => {
const instanceId = this.projects[cacheKey]
const target = InstanceIdMap[instanceId]
if (target) {
delete(InstanceIdMap[instanceId])
}
delete(this.projects[cacheKey])
})
}
get instanceIdMap() {
return InstanceIdMap
}
findProjectByInstanceId(instanceId) {
return InstanceIdMap[instanceId]
}
}
export default Portfolio
const FailureLog = (moduleId, stage, error) =>
`
There was an error loading the project at:
${moduleId}
It occured:
${ stage }
The Error Message was:
${ error.message }
The Stacktrace:
${ error.stack }
`
const passThroughLocalConfig = (project) => {
const main = project.skypagerMain
let localConfig = project.existsSync(project.resolve(main))
? attemptNormalRequire(project.resolve(main))
: (project) => project
if (isError(localConfig)) {
console.log(FailureLog(main, 'While requiring project config file', localConfig))
if(ARGV.fail || process.env.FAIL_FAST) {
process.exit(1)
}
} else if (isFunction(localConfig.default)) {
localConfig = localConfig.default
}
if (isFunction(localConfig)) {
try {
localConfig(project)
} catch(error) {
console.log(FailureLog(main, 'While running config function', error))
if(ARGV.fail || process.env.FAIL_FAST) {
process.exit(1)
}
}
}
try {
project.attachProjectTypes()
} catch(error) {
project.error('Error attaching project types', error)
}
try {
project.enableFeatures()
} catch(error) {
project.error('Error enabling features', error)
}
return project
}
const attemptNormalRequire = (moduleId, retryAgain = true) => {
try {
return __non_webpack_require__(moduleId)
} catch (error) {
if (retryAgain && error.message.match(/Unexpected token/i)) {
require('babel-register')
return attemptNormalRequire(moduleId, false)
}
return error
}
}