skypager-project
Version:
skypager project framework
600 lines (499 loc) • 14.2 kB
JavaScript
import lodash from 'lodash'
import { basename, sep, relative, join, resolve } from 'path'
import { mixinPropertyUtils } from 'skypager-util/lib/properties'
import query from 'skypager-util/lib/query'
import router from 'skypager-util/lib/router'
import Document from '../document'
import { toVFile as vfile } from '../importers/node-vinyl'
const { compact, set, groupBy, snakeCase, uniq, chain, mapValues } = lodash
const { values } = Object
import { isVinyl } from 'vinyl'
export class Collection {
static mount (cwd, options = {}) {
return create({
cwd, ...options,
}).imported.withDocuments
}
/**
* Create a collection which wraps a directory and creates a queryable index
* of all of the files inside of it. Files exist as VFile (@see https://github.com/wooorm/vfile) and are
* wrapped by Document classes which provide the interface for metadata, transformations, and things of that nature.
* A collection also provides a lodash wrapper interface for building extraction functions from the collections
* files and data.
*
* @param {Object} options - the options hash
* @param {String} options.type - the type of document - will determine which document model wrapper is used
* @param {String} options.cwd - the root of the collection, defaults to process.cwd()
* @param {String} options.cwd - the cwd, if different from the base
* @param {Array} options.include - an array of glob patterns to include
* @param {Array} options.exclude - an array of glob patterns to exclude
* @param {Object} options.documentOptions - an object that will be passed to the document wrapper when created
* @param {Object} options.importerOptions - an object that will be passed to the document importer when created
*/
constructor (options = {}) {
mixinPropertyUtils(this)
this.hide('cwd', options.cwd || options.root || options.base || options.basePath)
this.hide('options', normalize(options))
this.pathId = this.cwd.split('/').reverse().slice(0,2).join('-')
this.instanceId = `${this.pathId}-${Math.floor(Date.now() / 1000)}`
this.hideGetter('collection', options.collection)
this.hide('filesIndex', {})
this.hide('documentsIndex', {})
this.hide('project', options.project)
}
get gitInfo() {
try {
return require('../git')(this.cwd)
} catch(error) {
return {}
}
}
get importer() {
return new this.Importer(this.cwd, {
...this.importerOptions, patterns: this.patterns
})
}
get type() {
return snakeCase(this.options.type || 'Project').replace(/s$/i, '')
}
get files() {
return lodash.chain(this.filesIndex)
.values()
.compact()
.sortBy(d => d.path)
.value()
}
get documents() {
return lodash.chain(this.documentsIndex)
.values()
.compact()
.sortBy(d => d.file.path)
.value()
}
get documentClass() {
return this.options.documentClass || Document
}
get importerOptions() {
const c = this
return {
...c.options.importerOptions,
patterns: lodash.uniq([
...c.patterns || [],
...c.options.importerOptions.patterns || []
]),
}
}
get documentOptions() {
const collection = this
return {
collectionType: collection.type,
...collection.options.documentOptions || {},
cwd: collection.cwd,
}
}
get Document() {
return this.documentClass
}
get withDocuments() {
this.createDocuments()
return this
}
shouldRejectFile(file) {
if (typeof this.options.rejectFile === 'function') {
return this.options.rejectFile.call(this, file)
}
if (file.path.indexOf('/ios/') >= 0) {
return true
}
if (file.path.indexOf('/android/') >= 0) {
return true
}
return !file.path.startsWith(this.cwd)
}
createDocuments(documentOptions = {}) {
const Document = this.Document
lodash.mapValues(this.filesIndex, (file, path) => {
path = relative(this.cwd, path)
this.documentsIndex[path] = this.documentsIndex[path]
|| new Document(file, {
...this.documentOptions,
...documentOptions,
project: this.project,
collection: this,
})
})
return this
}
isDirectory(file) {
const path = file.path ? file.path : file
return typeof file.isDirectory === 'function'
? file.isDirectory()
: !path.match(/\.\w+$/)
}
add (file) {
if (file.path && !this.isDirectory(file)) {
lodash.assign(this.filesIndex, {
[relative(this.cwd, file.path)]: file
})
}
}
prepareImportedFilesForIndexing (files = []) {
const cwd = this.cwd
return lodash.chain(files)
.reject(f => this.isDirectory(f))
.reject(f => this.shouldRejectFile(f))
.compact()
.sortBy(f => f.path.length)
.keyBy(f => relative(cwd, f.path))
.value()
}
indexFiles(files = []) {
files.forEach(file => this.loadFile(file))
return this
}
loadFile(file = {}, cwd = this.cwd) {
lodash.assign(this.filesIndex, {
[this.resolve(cwd, file.path)]: this.prepareFileObject(file, cwd)
})
return this
}
prepareFileObject(file, cwd = this.cwd) {
return this.options.prepareFile ? this.options.prepareFile.call(this, file) : ensureVFile(file, cwd)
}
loadFiles(files = []) {
lodash.compact(files)
.filter(f => !this.isDirectory(f))
.forEach(f => this.loadFile(f))
return this
}
loadFilesFromImporter() {
return this.importer.fileWrappers
.then(files = this.prepareImportedFilesForIndexing(files))
.then(files => this.indexFiles(files.map(file => file.toVFile ? file.toVFile() : file)))
.then(collection => collection.withDocuments)
.catch(error => error)
}
get loaderPrefix() {
return this.get('options.loaderPrefix', '')
}
get documentEntryPoints() {
return this.chain
.get('documents')
.keyBy('id')
.mapValues((doc) => ([
`${this.loaderPrefix}${doc.file.path}`
]))
.mapValues(array => compact(array))
.value()
}
get documentsMappedById() {
return this.chain
.get('documents')
.keyBy('id')
.value()
}
get documentsMap () {
return this.documentsIndex
}
get Importer() {
return this.options.importer
? require(`../importers/` + this.options.importer + '.js')
: require('../importers/node-vinyl').DocumentImporter
}
get patterns() {
return lodash.uniq(
this.includePatterns.concat(this.excludePatterns)
)
}
get includePatterns() {
return lodash.uniq(
this.options.include.concat(this.typePatterns)
)
}
get typePatterns () {
switch (this.type.toUpperCase()) {
case PACKAGE:
return PatternMapping.Package
break
case SCRIPT:
return PatternMapping.Script
break
case SETTINGS_FILE, COPY_FILE:
return PatternMapping.SettingsFile
break
case DOCUMENT:
return PatternMapping.Document
break
case STYLESHEET:
return PatternMapping.Stylesheet
break
case VECTOR:
return PatternMapping.Vector
break
case PROJECT:
default:
return PatternMapping.Project
}
}
get router() {
return this.routers.documents
}
get routers() {
if (!this._routers) {
this.hide('_routers', {})
this.createRouter('files', {
pathProperty: 'relative'
})
this.createRouter('documents', {
pathProperty: 'baseRelativePath',
pathsGetter: 'documents',
})
this.createRouter('folders', {
pathProperty: 'toString',
pathsGetter: 'subFolderNames'
})
}
return mapValues(this._routers,
(router) => router.get.bind(router)
)
}
createRouter(name, options = {}) {
if (options.getter) { options.pathsGetter = options.getter}
if (options.property) { options.pathProperty = options.property}
router(this._routers, {
routerProperty: name,
pathsGetter: name,
pathProperty: 'id',
...options,
host: this,
matchesOnly: true,
})
}
createDelegators(receiver, ...propNames) {
propNames.forEach(propName => {
receiver.hideGetter(propName, () => this[propName])
})
}
getFileByRelativePath (relativeFilePath) {
const {index} = this.filesIndex[relativeFilePath]
return this.files[index]
}
join(...args) {
return join(this.cwd, ...args)
}
relative(...args) {
return relative(this.cwd, ...args)
}
resolve(...args) {
return resolve(this.cwd, ...args)
}
get chain() {
return chain(this)
}
select(...paths) {
return this.selectChain(...paths).value()
}
selectChain(...paths) {
const set = (obj,path,val) => {
lodash.set(obj,path,val)
return obj
}
return this.chain.at(...lodash.sortBy(paths, (p) => p.length))
.map((value,index) => [paths[index], value])
.reduce(((memo, pair) => set(memo, ...pair)), {})
}
selectByObject (pathObject) {
const keys = keys(pathObject)
const paths = values(pathObject)
return this.chain.at(...paths).reduce(
((memo,val,index) => set(memo, keys[index], val)),
{}
).value()
}
get relativeObjectPaths() {
return this.chain.get('files')
.sortBy(f => f.dirname.length)
.map((f) => [
this.relative(f.dirname).replace(/\//g, '.'),
f.basename,
f.relative
])
.groupBy(i => i.shift())
.value()
}
get subFolderObjectPaths() {
return this.chain.get('subFolderNames')
.sortBy(v => v.length)
.map(p => p.replace(/\//g,'.'))
.value()
}
get excludePatterns () {
if (this.options.includeAll || this.options.exclude === false) {
return []
}
return this.options.exclude
// HACK tmp
.concat(this.project ? this.project.skypagerIgnoreContents || [] : [])
.map(p => p.startsWith('!') ? p : `!${p}`)
.concat([
'!**/node_modules/**',
'*.xcodeproj',
'!_*',
])
}
get imported() {
if(this.files.length === 0) {
this.indexFiles(this.importer.readFilesSync(false))
}
return this
}
get rootFiles() {
return this.chains.files.filter(f => f.dirname === project.root).value()
}
get groupedByChildFolder() {
return this.chains.files
.groupBy(f => this.relative(f.dirname).split('/').shift())
.pickBy((v,k) => k.length > 0)
.value()
}
get groupedByChildFolderAndExtension() {
return this.chains.files
.groupBy(f => this.relative(f.dirname).split('/').shift())
.mapValues(v => groupBy(v, (v) => v.extname))
.pickBy((v,k) => k.length > 0)
.value()
}
get chains() {
return {
documents: chain(this.documents),
files: chain(this.files),
}
}
get query() {
const collection = this
return {
get files() {
return query.bind(query, collection.files)
},
get documents() {
return query.bind(query, collection.documents)
}
}
}
findDocumentById (documentId) {
return this.documents.find(doc => doc.id === documentId)
}
applyFilesRoute(pattern) {
const path = route()(pattern)
const results = this.files.map(f => ({path: f.relative, pathData: path(f.relative)})).filter(f => f.pathData)
return {
results,
pattern
}
}
get depths() {
return this.chains
.files.map(f => this.relative(f.dirname))
.uniq()
.groupBy(f => f.split(sep).length - 1)
.omit('')
.value()
}
get relativeFilePaths() {
return this.files.map(file => relative(this.cwd, file.path))
}
get relativeSubFolderPaths() {
return this.files.map(file => relative(this.cwd, file.dirname))
}
get subFolderPaths() {
return uniq(this.files.map(f => f.dirname))
}
get subFolderNames() {
return this.subFolderPaths
.map(f => relative(this.cwd, f))
.filter(f => f.length > 0)
}
get subFolderBasenames () {
return this.subFolderPaths
.map(f => relative(this.cwd, f))
.map(f => basename(f))
}
get childFolderNames() {
return uniq(
this.subFolderNames.map(p => p.split('/').shift())
)
}
get childFolderPaths() {
return uniq(
this.childFolderNames.map(
p => this.join(p)
)
)
}
get ready() {
return this.importer.status === 'LOADED'
? Promise.resolve(this)
: this.loadFilesFromImporter().then(() => this)
}
static create (options = {}, ready) {
const collection = new Collection(options)
return typeof ready === 'function'
? ready(collection, Collection)
: collection
}
static get Document() {
return Document
}
}
export default Collection
const PACKAGE = 'PACKAGE'
const SCRIPT = 'SCRIPT'
const DOCUMENT = 'DOCUMENT'
const SETTINGS_FILE = 'SETTINGS_FILE'
const COPY_FILE = 'COPY_FILE'
const PROJECT = 'PROJECT'
const VECTOR = 'VECTOR'
const STYLESHEET = 'STYLESHEET'
export const PatternMapping = {
'Package': ['**/package.json'],
'Document': ['**/*.md'],
'Script': ['**/*.js','**/*.jsx'],
'Stylesheet': ['**/*.{css,less,sass,scss,styl'],
'SettingsFile': ['**/*.yml'],
'CopyFile': ['**/*.yml'],
'Vector': ['**/*.svg'],
'DataSource': ['**/*.json','**/*.yml', '**/*.data.js'],
'Project': [
'**/*'
]
}
export function create(options = {}) {
return Collection.create(options)
}
function normalize (options = {}) {
return {
...options,
importerOptions: options.importerOptions || {},
documentOptions: options.documentOptions || {},
include: [...(options.include || []), ...(options.patterns || [])],
exclude: [...(options.exclude || []), ...(options.ignore || [])]
}
}
function ensureVFile(object = {}, cwd) {
if (cwd) {
object.cwd = cwd
}
if (typeof object.toVFile === 'function') {
return object.toVFile()
} else if (isVinyl(object)) {
return vfile(object)
} else if (typeof object === 'object' && object.contents && object.path) {
return vfile({
contents: new Buffer(object.contents.toString()),
path: object.path,
cwd: object.cwd,
})
} else {
return vfile(object)
}
}