diff --git a/gatsby-config.js b/gatsby-config.js
index 6979a3f2..27d8eec5 100644
--- a/gatsby-config.js
+++ b/gatsby-config.js
@@ -19,6 +19,15 @@ module.exports = {
path: `${__dirname}/src/images`,
},
},
+ {
+ resolve: "gatsby-source-lodash",
+ options: {
+ versions: [{
+ version: '4.17.11',
+ url: 'https://raw.githubusercontent.com/lodash/lodash/4.17.11-npm/lodash.js',
+ }]
+ },
+ },
`gatsby-plugin-react-svg`,
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
diff --git a/plugins/gatsby-source-lodash/README.md b/plugins/gatsby-source-lodash/README.md
new file mode 100644
index 00000000..f5c640e5
--- /dev/null
+++ b/plugins/gatsby-source-lodash/README.md
@@ -0,0 +1,25 @@
+# Gatsby Source Lodash
+
+A simple Gatsby Source Plugin to grab method metadata from Lodash.
+
+## Usage
+
+In the `gatsby-config.js` file, add `gatsby-source-lodash` to the `module.exports`.
+
+```js
+module.exports = {
+ plugins: [
+ ...
+ {
+ resolve: "gatsby-source-lodash",
+ options: {
+ versions: [{
+ version: '4.17.11',
+ url: 'https://raw.githubusercontent.com/lodash/lodash/4.17.11-npm/lodash.js',
+ }]
+ },
+ },
+ ...
+ ]
+}
+```
diff --git a/plugins/gatsby-source-lodash/gatsby-node.js b/plugins/gatsby-source-lodash/gatsby-node.js
new file mode 100644
index 00000000..eca7a5cf
--- /dev/null
+++ b/plugins/gatsby-source-lodash/gatsby-node.js
@@ -0,0 +1,101 @@
+"use strict"
+
+const fetch = require("node-fetch")
+const _ = require("lodash")
+const Entry = require("./lib/entry.js")
+const getEntries = Entry.getEntries
+
+exports.sourceNodes = (
+ { actions, createNodeId, createContentDigest },
+ configOptions
+) => {
+ const { createNode } = actions
+
+ // Gatsby adds a configOption that's not needed for this plugin, delete it
+ delete configOptions.plugins
+
+ // TODO: Extend this to include multiple versions
+ const url = configOptions.versions[0].url
+ const version = configOptions.versions[0].version
+
+ const processEntry = entry => {
+ try {
+ // Exit early if the entry is private or has no name
+ if (!entry || !entry.getName() || entry.isPrivate()) {
+ return
+ }
+ } catch (err) {
+ // Some of the non-lodash methods were throwing a hard to trace error
+ // from the lib code. Rather than trace back, it was easier to catch
+ // since they aren't part of lodash.
+ return
+ }
+
+ // Special handling of aliases to get call names. Without this, there are
+ // circular JS object references.
+ const aliases = entry.getAliases().map(alias => {
+ const member = entry.getMembers(0) || ""
+ const name = alias.getName()
+ return `${member}.${name}`
+ })
+
+ const params = entry
+ .getParams()
+ .map(([type, name, desc]) => ({ type, name, desc }))
+
+ const entryData = {
+ aliases,
+ call: entry.getCall(),
+ category: entry.getCategory(),
+ desc: entry.getDesc(),
+ example: entry.getExample(),
+ hash: entry.getHash(),
+ isAlias: entry.isAlias(),
+ isCtor: entry.isCtor(),
+ isFunction: entry.isFunction(),
+ isLicense: entry.isLicense(),
+ isPlugin: entry.isPlugin(),
+ isStatic: entry.isStatic(),
+ lineNumber: entry.getLineNumber(),
+ members: entry.getMembers(),
+ name: entry.getName(),
+ params,
+ related: entry.getRelated(),
+ returns: entry.getReturns(),
+ since: entry.getSince(),
+ type: entry.getType(),
+ version,
+ }
+
+ const nodeData = {
+ ...entryData,
+ children: [],
+ id: createNodeId(
+ `lodash_method_${version}_${entryData.hash}_${entryData.lineNumber}`
+ ),
+ internal: {
+ content: JSON.stringify(entryData),
+ contentDigest: createContentDigest(JSON.stringify(entryData)),
+ type: "LodashMethod",
+ },
+ parent: null,
+ }
+
+ return nodeData
+ }
+
+ return fetch(url)
+ .then(res => res.text())
+ .then(body => {
+ const entries = getEntries(body)
+
+ // For each entry, create node.
+ _.each(entries, entry => {
+ entry = new Entry(entry, body)
+ const nodeData = processEntry(entry)
+ if (nodeData) {
+ createNode(nodeData)
+ }
+ })
+ })
+}
diff --git a/plugins/gatsby-source-lodash/lib/alias.js b/plugins/gatsby-source-lodash/lib/alias.js
new file mode 100644
index 00000000..df3c7e72
--- /dev/null
+++ b/plugins/gatsby-source-lodash/lib/alias.js
@@ -0,0 +1,259 @@
+"use strict"
+
+const _ = require("lodash")
+
+/*----------------------------------------------------------------------------*/
+
+/**
+ * The Alias constructor.
+ *
+ * @constructor
+ * @param {string} name The alias name.
+ * @param {Object} owner The alias owner.
+ */
+function Alias(name, owner) {
+ this._owner = owner
+ this._name = name
+}
+
+/**
+ * Extracts the entry's `alias` objects.
+ *
+ * @memberOf Alias
+ * @param {number} [index] The index of the array value to return.
+ * @returns {Array|string} Returns the entry's `alias` objects.
+ */
+function getAliases(index) {
+ return index == null ? [] : undefined
+}
+
+/**
+ * Extracts the function call from the owner entry.
+ *
+ * @memberOf Alias
+ * @returns {string} Returns the function call.
+ */
+function getCall() {
+ return this._owner.getCall()
+}
+
+/**
+ * Extracts the owner entry's `category` data.
+ *
+ * @memberOf Alias
+ * @returns {string} Returns the owner entry's `category` data.
+ */
+function getCategory() {
+ return this._owner.getCategory()
+}
+
+/**
+ * Extracts the owner entry's description.
+ *
+ * @memberOf Alias
+ * @returns {string} Returns the owner entry's description.
+ */
+function getDesc() {
+ return this._owner.getDesc()
+}
+
+/**
+ * Extracts the owner entry's `example` data.
+ *
+ * @memberOf Alias
+ * @returns {string} Returns the owner entry's `example` data.
+ */
+function getExample() {
+ return this._owner.getExample()
+}
+
+/**
+ * Extracts the entry's hash value for permalinking.
+ *
+ * @memberOf Alias
+ * @param {string} [style] The hash style.
+ * @returns {string} Returns the entry's hash value (without a hash itself).
+ */
+function getHash(style) {
+ return this._owner.getHash(style)
+}
+
+/**
+ * Resolves the owner entry's line number.
+ *
+ * @memberOf Alias
+ * @returns {number} Returns the owner entry's line number.
+ */
+function getLineNumber() {
+ return this._owner.getLineNumber()
+}
+
+/**
+ * Extracts the owner entry's `member` data.
+ *
+ * @memberOf Alias
+ * @param {number} [index] The index of the array value to return.
+ * @returns {Array|string} Returns the owner entry's `member` data.
+ */
+function getMembers(index) {
+ return this._owner.getMembers(index)
+}
+
+/**
+ * Extracts the owner entry's `name` data.
+ *
+ * @memberOf Alias
+ * @returns {string} Returns the owner entry's `name` data.
+ */
+function getName() {
+ return this._name
+}
+
+/**
+ * Gets the owner entry object.
+ *
+ * @memberOf Alias
+ * @returns {Object} Returns the owner entry.
+ */
+function getOwner() {
+ return this._owner
+}
+
+/**
+ * Extracts the owner entry's `param` data.
+ *
+ * @memberOf Alias
+ * @param {number} [index] The index of the array value to return.
+ * @returns {Array} Returns the owner entry's `param` data.
+ */
+function getParams(index) {
+ return this._owner.getParams(index)
+}
+
+/**
+ * Extracts the owner entry's `returns` data.
+ *
+ * @memberOf Alias
+ * @returns {string} Returns the owner entry's `returns` data.
+ */
+function getReturns() {
+ return this._owner.getReturns()
+}
+
+/**
+ * Extracts the owner entry's `since` data.
+ *
+ * @memberOf Alias
+ * @returns {string} Returns the owner entry's `since` data.
+ */
+function getSince() {
+ return this._owner.getSince()
+}
+
+/**
+ * Extracts the owner entry's `type` data.
+ *
+ * @memberOf Alias
+ * @returns {string} Returns the owner entry's `type` data.
+ */
+function getType() {
+ return this._owner.getType()
+}
+
+/**
+ * Checks if the entry is an alias.
+ *
+ * @memberOf Alias
+ * @returns {boolean} Returns `true`.
+ */
+function isAlias() {
+ return true
+}
+
+/**
+ * Checks if the owner entry is a constructor.
+ *
+ * @memberOf Alias
+ * @returns {boolean} Returns `true` if a constructor, else `false`.
+ */
+function isCtor() {
+ return this._owner.isCtor()
+}
+
+/**
+ * Checks if the entry is a function reference.
+ *
+ * @memberOf Alias
+ * @returns {boolean} Returns `true` if the entry is a function reference, else `false`.
+ */
+function isFunction() {
+ return this._owner.isFunction()
+}
+
+/**
+ * Checks if the owner entry is a license.
+ *
+ * @memberOf Alias
+ * @returns {boolean} Returns `true` if a license, else `false`.
+ */
+function isLicense() {
+ return this._owner.isLicense()
+}
+
+/**
+ * Checks if the owner entry *is* assigned to a prototype.
+ *
+ * @memberOf Alias
+ * @returns {boolean} Returns `true` if assigned to a prototype, else `false`.
+ */
+function isPlugin() {
+ return this._owner.isPlugin()
+}
+
+/**
+ * Checks if the owner entry is private.
+ *
+ * @memberOf Alias
+ * @returns {boolean} Returns `true` if private, else `false`.
+ */
+function isPrivate() {
+ return this._owner.isPrivate()
+}
+
+/**
+ * Checks if the owner entry is *not* assigned to a prototype.
+ *
+ * @memberOf Alias
+ * @returns {boolean} Returns `true` if not assigned to a prototype, else `false`.
+ */
+function isStatic() {
+ return this._owner.isStatic()
+}
+
+/*----------------------------------------------------------------------------*/
+
+_.assign(Alias.prototype, {
+ getAliases,
+ getCall,
+ getCategory,
+ getDesc,
+ getExample,
+ getHash,
+ getLineNumber,
+ getMembers,
+ getName,
+ getOwner,
+ getParams,
+ getReturns,
+ getSince,
+ getType,
+ isAlias,
+ isCtor,
+ isFunction,
+ isLicense,
+ isPlugin,
+ isPrivate,
+ isStatic,
+})
+
+module.exports = Alias
diff --git a/plugins/gatsby-source-lodash/lib/entry.js b/plugins/gatsby-source-lodash/lib/entry.js
new file mode 100644
index 00000000..93201661
--- /dev/null
+++ b/plugins/gatsby-source-lodash/lib/entry.js
@@ -0,0 +1,581 @@
+"use strict"
+
+const _ = require("lodash")
+const fp = require("lodash/fp")
+const os = require("os")
+const Alias = require("./alias.js")
+const util = require("./util.js")
+
+/*----------------------------------------------------------------------------*/
+
+/**
+ * Gets the param type of `tag`.
+ *
+ * @private
+ * @param {Object} tag The param tag to inspect.
+ * @returns {string} Returns the param type.
+ */
+function getParamType(tag) {
+ let expression = tag.expression
+ let result = ""
+ const type = tag.type
+
+ switch (type) {
+ case "AllLiteral":
+ result = "*"
+ break
+
+ case "NameExpression":
+ result = _.toString(tag.name)
+ break
+
+ case "RestType":
+ result = "..." + result
+ break
+
+ case "TypeApplication":
+ expression = undefined
+ result = _(tag)
+ .chain()
+ .get("applications")
+ .map(
+ _.flow(
+ getParamType,
+ fp.add(fp, "[]")
+ )
+ )
+ .sort(util.compareNatural)
+ .join("|")
+ .value()
+ break
+
+ case "UnionType":
+ result = _(tag)
+ .chain()
+ .get("elements")
+ .map(getParamType)
+ .sort(util.compareNatural)
+ .join("|")
+ .value()
+ }
+ if (expression) {
+ result += getParamType(expression)
+ }
+ return type === "UnionType" ? "(" + result + ")" : result
+}
+
+/**
+ * Gets an `entry` tag by `tagName`.
+ *
+ * @private
+ * @param {Object} entry The entry to inspect.
+ * @param {string} tagName The name of the tag.
+ * @returns {null|Object} Returns the tag.
+ */
+function getTag(entry, tagName) {
+ const parsed = entry.parsed
+ return _.find(parsed.tags, ["title", tagName]) || null
+}
+
+/**
+ * Gets an `entry` tag value by `tagName`.
+ *
+ * @private
+ * @param {Object} entry The entry to inspect.
+ * @param {string} tagName The name of the tag.
+ * @returns {string} Returns the tag value.
+ */
+function getValue(entry, tagName) {
+ const parsed = entry.parsed
+ let result = parsed.description
+ const tag = getTag(entry, tagName)
+
+ if (tagName === "alias") {
+ result = _.get(tag, "name")
+
+ // Doctrine can't parse alias tags containing multiple values so extract
+ // them from the error message.
+ const error = _.first(_.get(tag, "errors"))
+ if (error) {
+ result += error.replace(/^[^']*'|'[^']*$/g, "")
+ }
+ } else if (tagName === "type") {
+ result = _.get(tag, "type.name")
+ } else if (tagName !== "description") {
+ result = _.get(tag, "name") || _.get(tag, "description")
+ }
+ return tagName === "example" ? _.toString(result) : util.format(result)
+}
+
+/**
+ * Checks if `entry` has a tag of `tagName`.
+ *
+ * @private
+ * @param {Object} entry The entry to inspect.
+ * @param {string} tagName The name of the tag.
+ * @returns {boolean} Returns `true` if the tag is found, else `false`.
+ */
+function hasTag(entry, tagName) {
+ return getTag(entry, tagName) !== null
+}
+
+/**
+ * Converts CR+LF line endings to LF.
+ *
+ * @private
+ * @param {string} str The string to convert.
+ * @returns {string} Returns the converted string.
+ */
+function normalizeEOL(str) {
+ return str.replace(/\r\n/g, "\n")
+}
+
+/*----------------------------------------------------------------------------*/
+
+/**
+ * The Entry constructor.
+ *
+ * @constructor
+ * @param {string} entry The documentation entry to analyse.
+ * @param {string} source The source code.
+ * @param {string} [lang='js'] The language highlighter used for code examples.
+ */
+function Entry(entry, source, lang) {
+ const normalizedEntry = normalizeEOL(entry)
+
+ this.entry = normalizedEntry
+ this.lang = lang == null ? "js" : lang
+ this.parsed = util.parse(normalizedEntry.replace(/(\*)\/\s*.+$/, "*"))
+ this.source = normalizeEOL(source)
+ this.getCall = _.memoize(this.getCall)
+ this.getCategory = _.memoize(this.getCategory)
+ this.getDesc = _.memoize(this.getDesc)
+ this.getExample = _.memoize(this.getExample)
+ this.getHash = _.memoize(this.getHash)
+ this.getLineNumber = _.memoize(this.getLineNumber)
+ this.getName = _.memoize(this.getName)
+ this.getRelated = _.memoize(this.getRelated)
+ this.getReturns = _.memoize(this.getReturns)
+ this.getSince = _.memoize(this.getSince)
+ this.getType = _.memoize(this.getType)
+ this.isAlias = _.memoize(this.isAlias)
+ this.isCtor = _.memoize(this.isCtor)
+ this.isFunction = _.memoize(this.isFunction)
+ this.isLicense = _.memoize(this.isLicense)
+ this.isPlugin = _.memoize(this.isPlugin)
+ this.isPrivate = _.memoize(this.isPrivate)
+ this.isStatic = _.memoize(this.isStatic)
+ this._aliases = this._members = this._params = undefined
+}
+
+/**
+ * Extracts the documentation entries from source code.
+ *
+ * @static
+ * @memberOf Entry
+ * @param {string} source The source code.
+ * @returns {Array} Returns the array of entries.
+ */
+function getEntries(source) {
+ return _.toString(source).match(/\/\*\*(?![-!])[\s\S]*?\*\/\s*.+/g) || []
+}
+
+/**
+ * Extracts the entry's `alias` objects.
+ *
+ * @memberOf Entry
+ * @param {number} index The index of the array value to return.
+ * @returns {Array|string} Returns the entry's `alias` objects.
+ */
+function getAliases(index) {
+ if (this._aliases === undefined) {
+ const owner = this
+ this._aliases = _(getValue(this, "alias"))
+ .split(/,\s*/)
+ .compact()
+ .sort(util.compareNatural)
+ .map(function(value) {
+ return new Alias(value, owner)
+ })
+ .value()
+ }
+ const result = this._aliases
+ return index === undefined ? result : result[index]
+}
+
+/**
+ * Extracts the function call from the entry.
+ *
+ * @memberOf Entry
+ * @returns {string} Returns the function call.
+ */
+function getCall() {
+ let result = _.trim(
+ _.get(/\*\/\s*(?:function\s+)?([^\s(]+)\s*\(/.exec(this.entry), 1)
+ )
+ if (!result) {
+ result = _.trim(_.get(/\*\/\s*(.*?)[:=,]/.exec(this.entry), 1))
+ result = /['"]$/.test(result)
+ ? _.trim(result, "\"'")
+ : result
+ .split(".")
+ .pop()
+ .split(/^(?:const|let|var) /)
+ .pop()
+ }
+ const name = getValue(this, "name") || result
+ if (!this.isFunction()) {
+ return name
+ }
+ const params = this.getParams()
+ result = _.castArray(result)
+
+ // Compile the function call syntax.
+ _.each(params, function(param) {
+ const paramValue = param[1]
+ const parentParam = _.get(/\w+(?=\.[\w.]+)/.exec(paramValue), 0)
+
+ const parentIndex =
+ parentParam === null
+ ? -1
+ : _.findIndex(params, function(parameter) {
+ return _.trim(parameter[1], "[]").split(/\s*=/)[0] === parentParam
+ })
+
+ // Skip params that are properties of other params (e.g. `options.leading`).
+ if (_.get(params[parentIndex], 0) !== "Object") {
+ result.push(paramValue)
+ }
+ })
+
+ // Format the function call.
+ return name + "(" + result.slice(1).join(", ") + ")"
+}
+
+/**
+ * Extracts the entry's `category` data.
+ *
+ * @memberOf Entry
+ * @returns {string} Returns the entry's `category` data.
+ */
+function getCategory() {
+ const result = getValue(this, "category")
+ return result || (this.getType() === "Function" ? "Methods" : "Properties")
+}
+
+/**
+ * Extracts the entry's description.
+ *
+ * @memberOf Entry
+ * @returns {string} Returns the entry's description.
+ */
+function getDesc() {
+ const type = this.getType()
+ const result = getValue(this, "description")
+
+ return !result || type === "Function" || type === "unknown"
+ ? result
+ : "(" + _.trim(type.replace(/\|/g, ", "), "()") + "): " + result
+}
+
+/**
+ * Extracts the entry's `example` data.
+ *
+ * @memberOf Entry
+ * @returns {string} Returns the entry's `example` data.
+ */
+function getExample() {
+ const result = getValue(this, "example")
+ return result && "```" + this.lang + "\n" + result + "\n```"
+}
+
+/**
+ * Extracts the entry's hash value for permalinking.
+ *
+ * @memberOf Entry
+ * @param {string} [style] The hash style.
+ * @returns {string} Returns the entry's hash value (without a hash itself).
+ */
+function getHash(style) {
+ let result = _.toString(this.getMembers(0))
+ if (style === "github") {
+ if (result) {
+ result += this.isPlugin() ? "prototype" : ""
+ }
+ result += this.getCall()
+ return result
+ .replace(/[\\.=|'"(){}\[\]\t ]/g, "")
+ .replace(/[#,]+/g, "-")
+ .toLowerCase()
+ }
+ if (result) {
+ result += "-" + (this.isPlugin() ? "prototype-" : "")
+ }
+ result += this.isAlias() ? this.getOwner().getName() : this.getName()
+ return result.replace(/\./g, "-").replace(/^_-/, "")
+}
+
+/**
+ * Resolves the entry's line number.
+ *
+ * @memberOf Entry
+ * @returns {number} Returns the entry's line number.
+ */
+function getLineNumber() {
+ const lines = this.source
+ .slice(0, this.source.indexOf(this.entry) + this.entry.length)
+ .match(/\n/g)
+ .slice(1)
+
+ // Offset by 2 because the first line number is before a line break and the
+ // last line doesn't include a line break.
+ return lines.length + 2
+}
+
+/**
+ * Extracts the entry's `member` data.
+ *
+ * @memberOf Entry
+ * @param {number} [index] The index of the array value to return.
+ * @returns {Array|string} Returns the entry's `member` data.
+ */
+function getMembers(index) {
+ if (this._members === undefined) {
+ this._members = _(getValue(this, "member") || getValue(this, "memberOf"))
+ .split(/,\s*/)
+ .compact()
+ .sort(util.compareNatural)
+ .value()
+ }
+ const result = this._members
+ return index === undefined ? result : result[index]
+}
+
+/**
+ * Extracts the entry's `name` data.
+ *
+ * @memberOf Entry
+ * @returns {string} Returns the entry's `name` data.
+ */
+function getName() {
+ return hasTag(this, "name")
+ ? getValue(this, "name")
+ : _.toString(_.first(this.getCall().split("(")))
+}
+
+/**
+ * Extracts the entry's `param` data.
+ *
+ * @memberOf Entry
+ * @param {number} [index] The index of the array value to return.
+ * @returns {Array} Returns the entry's `param` data.
+ */
+function getParams(index) {
+ if (this._params === undefined) {
+ this._params = _(this.parsed.tags)
+ .filter(["title", "param"])
+ .filter("name")
+ .map(function(tag) {
+ const defaultValue = tag["default"]
+ const desc = util.format(tag.description)
+ let name = _.toString(tag.name)
+ const type = getParamType(tag.type)
+
+ if (defaultValue != null) {
+ name += "=" + defaultValue
+ }
+ if (_.get(tag, "type.type") === "OptionalType") {
+ name = "[" + name + "]"
+ }
+ return [type, name, desc]
+ })
+ .value()
+ }
+ const result = this._params
+ return index === undefined ? result : result[index]
+}
+
+/**
+ * Extracts the entry's `see` data.
+ *
+ * @memberOf Entry
+ * @returns {array} Returns the entry's `see` data as links.
+ */
+function getRelated() {
+ const relatedValues = getValue(this, "see")
+ if (relatedValues && relatedValues.trim().length > 0) {
+ const relatedItems = relatedValues
+ .split(",")
+ .map(relatedItem => relatedItem.trim())
+ return relatedItems.map(
+ relatedItem => "[" + relatedItem + "](#" + relatedItem + ")"
+ )
+ } else {
+ return []
+ }
+}
+
+/**
+ * Extracts the entry's `returns` data.
+ *
+ * @memberOf Entry
+ * @returns {array} Returns the entry's `returns` data.
+ */
+function getReturns() {
+ const tag = getTag(this, "returns")
+ const desc = _.toString(_.get(tag, "description"))
+ const type = _.toString(_.get(tag, "type.name")) || "*"
+
+ return tag ? [type, desc] : []
+}
+
+/**
+ * Extracts the entry's `since` data.
+ *
+ * @memberOf Entry
+ * @returns {string} Returns the entry's `since` data.
+ */
+function getSince() {
+ return getValue(this, "since")
+}
+
+/**
+ * Extracts the entry's `type` data.
+ *
+ * @memberOf Entry
+ * @returns {string} Returns the entry's `type` data.
+ */
+function getType() {
+ const result = getValue(this, "type")
+ if (!result) {
+ return this.isFunction() ? "Function" : "unknown"
+ }
+ return /^(?:array|function|object|regexp)$/.test(result)
+ ? _.capitalize(result)
+ : result
+}
+
+/**
+ * Checks if the entry is an alias.
+ *
+ * @memberOf Entry
+ * @type {Function}
+ * @returns {boolean} Returns `false`.
+ */
+const isAlias = _.constant(false)
+
+/**
+ * Checks if the entry is a constructor.
+ *
+ * @memberOf Entry
+ * @returns {boolean} Returns `true` if a constructor, else `false`.
+ */
+function isCtor() {
+ return hasTag(this, "constructor")
+}
+
+/**
+ * Checks if the entry is a function reference.
+ *
+ * @memberOf Entry
+ * @returns {boolean} Returns `true` if the entry is a function reference, else `false`.
+ */
+function isFunction() {
+ return !!(
+ this.isCtor() ||
+ _.size(this.getParams()) ||
+ _.size(this.getReturns()) ||
+ hasTag(this, "function") ||
+ /\*\/\s*(?:function\s+)?[^\s(]+\s*\(/.test(this.entry)
+ )
+}
+
+/**
+ * Checks if the entry is a license.
+ *
+ * @memberOf Entry
+ * @returns {boolean} Returns `true` if a license, else `false`.
+ */
+function isLicense() {
+ return hasTag(this, "license")
+}
+
+/**
+ * Checks if the entry *is* assigned to a prototype.
+ *
+ * @memberOf Entry
+ * @returns {boolean} Returns `true` if assigned to a prototype, else `false`.
+ */
+function isPlugin() {
+ return !this.isCtor() && !this.isPrivate() && !this.isStatic()
+}
+
+/**
+ * Checks if the entry is private.
+ *
+ * @memberOf Entry
+ * @returns {boolean} Returns `true` if private, else `false`.
+ */
+function isPrivate() {
+ return (
+ this.isLicense() || hasTag(this, "private") || _.isEmpty(this.parsed.tags)
+ )
+}
+
+/**
+ * Checks if the entry is *not* assigned to a prototype.
+ *
+ * @memberOf Entry
+ * @returns {boolean} Returns `true` if not assigned to a prototype, else `false`.
+ */
+function isStatic() {
+ const isPublic = !this.isPrivate()
+ let result = isPublic && hasTag(this, "static")
+
+ // Get the result in cases where it isn't explicitly stated.
+ if (isPublic && !result) {
+ const parent = _.last(_.toString(this.getMembers(0)).split(/[#.]/))
+ if (!parent) {
+ return true
+ }
+ const source = this.source
+ _.each(getEntries(source), function(entry) {
+ entry = new Entry(entry, source)
+ if (entry.getName() === parent) {
+ result = !entry.isCtor()
+ return false
+ }
+ })
+ }
+ return result
+}
+
+/*----------------------------------------------------------------------------*/
+
+Entry.getEntries = getEntries
+
+_.assign(Entry.prototype, {
+ getAliases,
+ getCall,
+ getCategory,
+ getDesc,
+ getExample,
+ getHash,
+ getLineNumber,
+ getMembers,
+ getName,
+ getParams,
+ getRelated,
+ getReturns,
+ getSince,
+ getType,
+ isAlias,
+ isCtor,
+ isFunction,
+ isLicense,
+ isPlugin,
+ isPrivate,
+ isStatic,
+})
+
+module.exports = Entry
diff --git a/plugins/gatsby-source-lodash/lib/util.js b/plugins/gatsby-source-lodash/lib/util.js
new file mode 100644
index 00000000..d731ee35
--- /dev/null
+++ b/plugins/gatsby-source-lodash/lib/util.js
@@ -0,0 +1,98 @@
+"use strict"
+
+const _ = require("lodash")
+const doctrine = require("doctrine")
+
+const reCode = /`.*?`/g
+const reToken = /@@token@@/g
+const split = String.prototype.split
+const token = "@@token@@"
+
+/*----------------------------------------------------------------------------*/
+
+/**
+ * The `Array#sort` comparator to produce a
+ * [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order).
+ *
+ * @memberOf util
+ * @param {*} value The value to compare.
+ * @param {*} other The other value to compare.
+ * @returns {number} Returns the sort order indicator for `value`.
+ */
+function compareNatural(value, other) {
+ let index = -1
+ const valParts = split.call(value, ".")
+ const valLength = valParts.length
+ const othParts = split.call(other, ".")
+ const othLength = othParts.length
+ const length = Math.min(valLength, othLength)
+
+ while (++index < length) {
+ const valPart = valParts[index]
+ const othPart = othParts[index]
+
+ if (valPart > othPart && othPart !== "prototype") {
+ return 1
+ } else if (valPart < othPart && valPart !== "prototype") {
+ return -1
+ }
+ }
+ return valLength > othLength ? 1 : valLength < othLength ? -1 : 0
+}
+
+/**
+ * Performs common string formatting operations.
+ *
+ * @memberOf util
+ * @param {string} str The string to format.
+ * @returns {string} Returns the formatted string.
+ */
+function format(str) {
+ let copy = _.toString(str)
+
+ // Replace all code snippets with a token.
+ let snippets = []
+ copy = copy.replace(reCode, function(match) {
+ snippets.push(match)
+ return token
+ })
+
+ return (
+ copy
+ // Add line breaks.
+ .replace(/:\n(?=[\t ]*\S)/g, ":
\n")
+ .replace(/\n( *)[-*](?=[\t ]+\S)/g, "\n
\n$1*")
+ .replace(/^[\t ]*\n/gm, "
\n
\n")
+ // Normalize whitespace.
+ .replace(/\n +/g, " ")
+ // Italicize parentheses.
+ .replace(/(^|\s)(\(.+\))/g, "$1*$2*")
+ // Mark numbers as inline code.
+ .replace(/[\t ](-?\d+(?:.\d+)?)(?!\.[^\n])/g, " `$1`")
+ // Replace all tokens with code snippets.
+ .replace(reToken, function(match) {
+ return snippets.shift()
+ })
+ .trim()
+ )
+}
+
+/**
+ * Parses the JSDoc `comment` into an object.
+ *
+ * @memberOf util
+ * @param {string} comment The comment to parse.
+ * @returns {Object} Returns the parsed object.
+ */
+const parse = _.partial(doctrine.parse, _, {
+ lineNumbers: true,
+ recoverable: true,
+ sloppy: true,
+ unwrap: true,
+})
+
+module.exports = {
+ compareNatural,
+ format,
+ parse,
+}
diff --git a/plugins/gatsby-source-lodash/package-lock.json b/plugins/gatsby-source-lodash/package-lock.json
new file mode 100644
index 00000000..08f8f3e1
--- /dev/null
+++ b/plugins/gatsby-source-lodash/package-lock.json
@@ -0,0 +1,31 @@
+{
+ "name": "gatsby-source-lodash",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "esutils": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
+ },
+ "lodash": {
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+ },
+ "node-fetch": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
+ "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA=="
+ }
+ }
+}
diff --git a/plugins/gatsby-source-lodash/package.json b/plugins/gatsby-source-lodash/package.json
new file mode 100644
index 00000000..5152ab02
--- /dev/null
+++ b/plugins/gatsby-source-lodash/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "gatsby-source-lodash",
+ "version": "0.1.0",
+ "description": "Gatsby Source Plugin to grab method metadata from Lodash.",
+ "main": "gatsby-node.js",
+ "scripts": {},
+ "keywords": [],
+ "author": "Zack Hall ",
+ "contributors": [
+ "John-David Dalton ",
+ "Mathias Bynens ",
+ "Zack Hall "
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "doctrine": "^3.0.0",
+ "lodash": "^4.17.11",
+ "node-fetch": "^2.3.0"
+ }
+}