UNPKG

skypager-project

Version:
600 lines (499 loc) 14.2 kB
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) } }