UNPKG

skypager-project

Version:
342 lines (264 loc) 7.67 kB
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 } }