UNPKG

skypager-project

Version:
565 lines (461 loc) 13.9 kB
import url from 'url' import { parse as parsePath, sep, basename, resolve, relative } from 'path' import { mixinPropertyUtils } from 'skypager-util/lib/properties' import * as stringUtils from 'skypager-util/lib/string' import isAbsolutePath from 'path-is-absolute' import Collection from '../collection' import ContextRegistry from 'skypager-registry/lib/context' import Cache from '../cache' import mapValues from 'lodash/mapValues' import isFunction from 'lodash/isFunction' import vfile from 'vfile' import set from 'lodash/set' import omit from 'lodash/omit' import pick from 'lodash/pick' import uniq from 'lodash/uniq' import isEmpty from 'lodash/isEmpty' import castArray from 'lodash/castArray' import hashObject from 'object-hash' export const PATH_SEPARATOR = sep export const DefaultPatterns = [ '**/*.{js,md,yml,json,svg,html,css,scss,less,json}', '!node_modules/**' ] export const patterns = DefaultPatterns export class Document { static patterns = DefaultPatterns; static mountCollection(cwd, options = {}) { if (!cwd) { throw new Error('Must pass a folder to Document.mountCollection') } return Collection.mount(cwd, options) } static cache = new Cache() static selectors = new ContextRegistry('documentSelectors', { context: require.context('../selectors/document', true, /\.js$/), wrapper: function(fn, id) { return (doc, ...args) => Document.cache.fetch(`selectors:${id}:${hashObject({args})}:${doc.cacheKey}`, () => fn.call(doc, doc.exporter, ...args).value()) } }) constructor (vFileOrPath, options = {}) { mixinPropertyUtils(this) if (options.project) { this.hide('project', options.project) delete(options.project) } if (options.collection) { this.hide('collection', options.collection) delete(options.collection) } this.hide('options', options) this.hide('cwd', options.cwd) const originalURI = normalizeUri( vFileOrPath.path ? vFileOrPath.path : vFileOrPath, options.cwd ) this.hide('originalURI', originalURI) this.hide('parsedURI', url.parse(this.originalURI)) this.hide('url', this.parsedURI.href) this.hide('uri', this.url) this.hide('path', unescape(this.parsedURI.path)) this.hide('_appliedDocTypes', {}) if (vFileOrPath.path) { this.options.file = vFileOrPath } this.lazy('file', () => this.toVFile({ cwd: this.cwd, ...(this.options.file || {}) })) this.hide('data', {}) this.lazy('id', this.buildId.bind(this)) this.lazy('fileStat', () => this.get('file.stat', {})) this.lazy('createdAt', () => this.result('fileStat.ctime', () => Math.floor(new Date() / 1000))) this.lazy('pathInfo', () => parsePath(this.path)) this.lazy('vmSandbox', () => this.project.vm.createContext({ ...this.project.sandbox, doc: this, global: {}, })) } get appliedDocTypes() { return Object.keys(this._appliedDocTypes) } get updatedAt() { return Math.floor(this.get('data.updatedAt', this.createdAt) / 1000) } touch() { try { this.set('data.updatedAt', Math.floor(new Date() / 1000)) } catch(e) { } return this } get decorated() { this.applyDoctypes() return this } createContext(sandbox = {}) { return { ...sandbox, doc: this, project: this.project, } } relativeDoc(relativeDocId) { return this.project.documentsMap[ this.project.relative( this.pathToRelative( relativeDocId ) ) ] } pathToRelative(relativePath) { return resolve(this.fileDirname, relativePath) } getTransformPresets() { return this.getTransforms() } getTransforms() { return this.matchingDocTypes .reduce((memo, docType) => ({ ...memo, [docType.name]: docType.getTransforms ? docType.getTransforms() : {}, }), {}) } getUtils() { return this.matchingDocTypes .reduce((memo, docType) => ({ ...memo, ...docType.getUtils ? docType.getUtils() : {}, })) } buildId (usingPath = this.path, cwd = this.cwd) { if (isFunction(this.options.buildId)) { return this.options.buildId( relative(cwd, usingPath) ) } else if (this.collection && isFunction(this.collection.buildId)) { return this.collection.buildId( relative(cwd, usingPath) ) } else { return relative(cwd, usingPath) .replace(/\.\w+$/g, '') .replace(/\.js$/g,'') } } get selectors() { return this.constructor.selectors } select(selector, ...args) { if (isFunction(selector)) { const result = selector.call(this, this.exporter, ...args) return result && typeof result.value === 'function' ? result.value() : result } else if (typeof selector === 'string') { return this.constructor.selectors.lookup(selector)(this, ...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 } get chain() { return this.project.chain.plant(this) } get exporter() { return this.project.chain.plant(this.ready) } get ready() { return this.readFile(true).decorated } get doctypes() { return this.docTypes } get docTypes() { const { project } = this return project.chain .get('documentTypes.available', []) .thru(list => list.map(docType => project.documentType(docType))) .value() } get matchingDocTypes() { const { project } = this return project.chain .get('documentTypes.available', []) .thru(list => list.map(docType => project.documentType(docType))) .flatten() .filter(docType => docType.testDocument(this)) .value() } sliceId(removeFromBeginning = 1, removeFromEnd = 1) { if (removeFromEnd < 0) { removeFromEnd = removeFromEnd * -1 } return this.idParts.slice(removeFromBeginning, removeFromEnd < 0 ? removeFromEnd * -1 : this.idParts.length) } get idParts() { return this.id.split(PATH_SEPARATOR) } get baseFolder() { return this.idParts[0] } get categoryFolderName() { const length = this.idParts.length return this.isIndex ? this.idParts[length - 3] || this.idParts[1] : this.idParts[length - 2] || this.idParts[1] } get categoryFolder() { return (this.isIndex ? this.idParts.slice(0, this.idparts.length - 2) : this.idParts.slice(0, this.idparts.length - 1)).join(PATH_SEPARATOR) } applyDoctypes() { if (isEmpty(this.appliedDocTypes)) { this.matchingDocTypes.forEach(docType => { docType.decorate(this) this.appliedDocTypes[docType.name] = true }) } return this } toJson (options = {}) { return { id: this.id, file: omit(this.file, 'contents', '_contents'), metadata: this.metadata, idParts: this.idParts, categoryFolder: this.categoryFolder, categoryFolderName: this.categoryFolderName, baseFolder: this.baseFolder, variations: this.variations, ...(options.pick ? pick(this, options.pick) : {}), ...(options.pickBy ? pickBy(this, options.pickBy) : {}), ...(options.includeContents ? {contents: this.file.contents} : {contents: null}), } } toVFile(fileOptions = {}) { return vfile({ path: this.path, cwd: this.cwd, ...fileOptions, }) } readFile(cache = true) { if (cache && this.contents && this.contents.length > 0) { this.file.contents = this.contents return this } this.file.contents = this.contents = this.project.readFileSync(this.file.path).toString() return this.touch() } readFileAsync() { return this.project.readFileAsync(this.file.path) .then((contents) => this.contents = contents.toString()) } get metadata() { return { ...this.fileData, ...this.data, ...this.get('exportables.frontmatter', {}), } } get idPair() { return [this.id, this] } get isIndex() { return !!( this.id.match(/index$/) || this.fileStem === 'index' || this.fileBaseName.startsWith('index') ) } get isPackageManifest() { return this.fileBaseName === 'package.json' } get variationsBase() { return this.isIndex ? this.fileDirnameBasename : this.fileStem } get fileExtname() { return this.file.extname } get isMarkdown() { return this.fileExtname && this.fileExtname.match(/.md$/) } get isJavascript() { return this.fileExtname && this.fileExtname.match(/.js$/) } /** * 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) => { set(memo, prop, this.result(prop)) return memo }, {}) } createEntityFrom(...properties) { const src = this.ready.slice(...properties) return require('../entity').createEntity(src) } asyncRunner(code, options = {}, context = {}) { return this.project.createSandboxedScriptRunner(code, { sandbox: this.vmSandbox }) } createStemVariations (base = this.variationsBase, options = {}) { const { startCase, camelCase, kebabCase, snakeCase, humanize, titleize } = stringUtils return mapValues({ classified: startCase(camelCase(base)).replace(/\s+/g,''), camel: camelCase(base), kebab: kebabCase(base), snake: snakeCase(base), underscored: kebabCase(base).replace(/\-/g,'_'), humanized: humanize(kebabCase(base).replace(/\-/g,'_')), titleized: titleize(humanize(kebabCase(base).replace(/\-/g,'_'))), }, (v) => { const value = `${options.prefix || ''}${v}${options.suffix || ''}` const modifier = options.pluralize ? stringUtils.pluralize : options.singularize ? stringUtils.singularize : ((v) => v) return modifier(value) }) } get fileData () { return isFunction(this.options.getFileData) ? this.options.getFileData.call(this, this.file) : this.result('file.data', () => Object.assign(this.file, { data: {}, })) } get fileMessages () { return this.get('file.messages', {}) } get variationsWithDirname () { const base = `${this.fileDirnameBasename}_${this.fileStem}` return { ...this.createStemVariations(base), singularized: this.createStemVariations(base, { singularize: true, }), pluralized: this.createStemVariations(base, { pluralize: true, }), } } get variations () { return { ...this.createStemVariations(this.variationsBase), singularized: this.createStemVariations(this.variationsBase, { singularize: true, }), pluralized: this.createStemVariations(this.variationsBase, { pluralize: true, }), } } get nameVariations() { const variations = this.variations return uniq(Object.values(omit(variations, 'pluralized', 'singularized')).concat( Object.values(variations.pluralized), Object.values(variations.singularized) )) } get indexVariations() { return this.createStemVariations(this.variationsBase, { suffix: `/index${this.fileExtname}`, }) } get filenameVariations() { return { ...this.createStemVariations(this.variationsBase, { suffix: this.fileExtname, }) } } get fileStem() { return this.file.stem } get fileBaseName() { return this.file.basename } get fileDirname () { return this.file.dirname } get relativeDirname() { return relative( this.cwd, this.fileDirname ) } get fileDirnameBasename () { return basename(this.fileDirname) } get baseRelativePath() { return relative( this.cwd, this.path ) } get contentLength() { const statSize = this.result('stat.size') if (typeof statSize === 'undefined') { return 0 } else { return (this.get('contents', this.get('file.contents', '')) || '').length } } get stat() { return this.get('file.stat', {}) } get cacheKey() { return [ this.baseRelativePath, this.updatedAt.toString(), this.contentLength.toString() ].join(':') } get isRemote() { return this.uri.protocol.indexOf('http') !== -1 } reloadFileContents() { return this.contents = this.file.contents = this.readFileSync(false) } getFileContents() { return this.result('file.contents', '').toString() } } export default Document export function normalizeUri (input, cwd = process.cwd()) { if (typeof input !== 'string') { throw('Must pass the URI as a string') } const parsed = url.parse(input) const isFilePath = parsed.protocol === null || parsed.protocol.indexOf('file') !== -1 const isRemoteUrl = !isFilePath && parsed.protocol.indexOf('http') !== -1 const isAbsoluteFilePath = isFilePath && isAbsolutePath(parsed.path) if (isRemoteUrl) { return url.format(parsed) } if (isFilePath && isAbsoluteFilePath) { return url.format({ protocol: 'file:', slashes: true, pathname: parsed.path, }) } else { return url.format({ protocol: 'file:', slashes: true, pathname: resolve(cwd, parsed.path), }) } return input }