Skip to content

Instantly share code, notes, and snippets.

@mysticatea
Created January 17, 2017 04:40
Show Gist options
  • Save mysticatea/ff83097cdebb0aad79fe6cccc29d4f50 to your computer and use it in GitHub Desktop.
Save mysticatea/ff83097cdebb0aad79fe6cccc29d4f50 to your computer and use it in GitHub Desktop.
Generators version of TokenStore. This was 15% slower than the cursor class version.
/**
* @fileoverview Object to handle access and retrieval of tokens.
* @author Brandon Mills
*/
"use strict";
//------------------------------------------------------------------------------
// Implementation
//------------------------------------------------------------------------------
const PUBLIC_METHODS = Object.freeze([
"getTokenByRangeStart",
"getFirstToken",
"getLastToken",
"getTokenBefore",
"getTokenAfter",
"getFirstTokenBetween",
"getLastTokenBetween",
"getFirstTokens",
"getLastTokens",
"getTokensBefore",
"getTokensAfter",
"getFirstTokensBetween",
"getLastTokensBetween",
"getTokens",
"getTokensBetween",
"getTokenOrCommentBefore",
"getTokenOrCommentAfter"
]);
/**
* Binary-searches the index of the first token which is after the given location.
* If it was not found, this returns `tokens.length`.
*
* @param {(Token|Comment)[]} tokens - It searches the token in this list.
* @param {number} location - The location to search.
* @returns {number} The found index or `tokens.length`.
* @private
*/
function search(tokens, location) {
let left = 0;
let right = tokens.length - 1;
while (left <= right) {
const middle = (left + right) / 2 | 0;
const value = tokens[middle].range[0];
if (value < location) {
left = middle + 1;
} else if (value > location) {
right = middle - 1;
} else {
return middle;
}
}
return Math.max(left, right);
}
function getStartOffset(tokens, index) {
return (index < tokens.length) ? tokens[index].range[0] : Number.MAX_SAFE_INTEGER;
}
function getEndOffset(tokens, index) {
return (index >= 0) ? tokens[index].range[1] : 0;
}
function isBefore(tokens, index, border, end) {
return (
index < tokens.length &&
tokens[index].range[0] < border &&
tokens[index].range[1] <= end
);
}
function isAfter(tokens, index, border, start) {
return (
index >= 0 &&
tokens[index].range[1] > border &&
tokens[index].range[0] >= start
);
}
const iterationFwd = {
*tokens(tokens, comments, imap, start, end) {
for (let i = imap.firstOf(start), last = imap.lastOf(end); i <= last; ++i) {
yield tokens[i];
}
},
*tokensAndComments(tokens, comments, imap, start, end) {
let t = imap.firstOf(start);
let c = search(comments, start);
let border = 0;
while ((t < tokens.length && tokens[t].range[1] <= end) || (c < comments.length && comments[c].range[1] <= end)) {
border = getStartOffset(comments, c);
while (isBefore(tokens, t, border, end)) {
yield tokens[t];
t += 1;
}
border = getStartOffset(tokens, t);
while (isBefore(comments, c, border, end)) {
yield comments[c];
c += 1;
}
}
}
};
const iterationBwd = {
*tokens(tokens, comments, imap, start, end) {
for (let i = imap.lastOf(end), first = imap.firstOf(start); i >= first; --i) {
yield tokens[i];
}
},
*tokensAndComments(tokens, comments, imap, start, end) {
let t = imap.lastOf(end);
let c = search(comments, end) - 1;
let border = 0;
while ((t >= 0 && tokens[t].range[0] >= start) || (c >= 0 && comments[c].range[0] >= start)) {
border = getEndOffset(comments, c);
while (isAfter(tokens, t, border, start)) {
yield tokens[t];
t -= 1;
}
border = getEndOffset(tokens, t);
while (isAfter(comments, c, border, start)) {
yield comments[c];
c -= 1;
}
}
}
};
function *iterateTokensPad(tokens, comments, imap, start, end, beforeCount, afterCount) {
for (let i = Math.max(0, imap.firstOf(start) - beforeCount), last = Math.min(tokens.length - 1, imap.lastOf(end) + afterCount); i <= last; ++i) {
yield tokens[i];
}
}
function *filter(iterable, predicate) {
for (const token of iterable) {
if (predicate(token)) {
yield token;
}
}
}
function *skip(iterable, count) {
let i = 0;
for (const token of iterable) {
if (++i > count) {
yield token;
}
}
}
function *take(iterable, count) {
let i = 0;
for (const token of iterable) {
if (++i > count) {
break;
}
yield token;
}
}
/**
* Iterates tokens with normalized options.
*
* @param {Function} iterateTokens - The function iterate tokens only.
* @param {Function} iterateTokensAndComments - The function iterate tokens and comments.
* @param {Token[]} tokens - The array of tokens.
* @param {Comment[]} comments - The array of comments.
* @param {IndexMap} imap - The map from locations to indices in `this.tokens`.
* @param {number} start - The start location of the iteration range.
* @param {number} end - The end location of the iteration range.
* @param {boolean} includeComments - The flag to iterate comments as well.
* @param {Function|null} predicate - The predicate function to choose tokens.
* @param {number} skipCount - The count of tokens the iterator skips.
* @param {number} takeCount - The maximum count of tokens the iterator iterates. Zero is no iteration for backward compatibility.
* @returns {IterableIterator<(Token|Comment)>} The created iterator.
* @private
*/
function iterateWith(iteration, tokens, comments, imap, start, end, includeComments, predicate, skipCount, takeCount) {
const iterate = includeComments ? iteration.tokensAndComments : iteration.tokens;
let it = iterate(tokens, comments, imap, start, end);
if (predicate) {
it = filter(it, predicate);
}
if (skipCount >= 1) {
it = skip(it, skipCount);
}
if (takeCount >= 0) {
it = take(it, takeCount);
}
return it;
}
/**
* Iterates tokens with skip options.
*
* @param {Function} iterateTokens - The function iterate tokens only.
* @param {Function} iterateTokensAndComments - The function iterate tokens and comments.
* @param {Token[]} tokens - The array of tokens.
* @param {Comment[]} comments - The array of comments.
* @param {Object} imap - The map from locations to indices in `this.tokens`.
* @param {number} start - The start location of the iteration range.
* @param {number} end - The end location of the iteration range.
* @param {number|Function|Object} [opts=0] - The option object. If this is a number then it's `opts.skip`. If this is a function then it's `opts.filter`.
* @param {boolean} [opts.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [opts.filter=null] - The predicate function to choose tokens.
* @param {number} [opts.skip=0] - The count of tokens the iterator skips.
* @returns {IterableIterator<(Token|Comment)>} The created iterator.
* @private
*/
function iterateWithSkip(iteration, tokens, comments, imap, start, end, opts) {
let includeComments = false;
let skipCount = 0;
let predicate = null;
if (typeof opts === "number") {
skipCount = opts | 0;
} else if (typeof opts === "function") {
predicate = opts;
} else if (opts) {
includeComments = !!opts.includeComments;
skipCount = opts.skip | 0;
predicate = opts.filter || null;
}
return iterateWith(iteration, tokens, comments, imap, start, end, includeComments, predicate, skipCount, -1);
}
/**
* Iterates tokens with count options.
*
* @param {Function} iterateTokens - The function iterate tokens only.
* @param {Function} iterateTokensAndComments - The function iterate tokens and comments.
* @param {Token[]} tokens - The array of tokens.
* @param {Comment[]} comments - The array of comments.
* @param {Object} imap - The map from locations to indices in `this.tokens`.
* @param {number} start - The start location of the iteration range.
* @param {number} end - The end location of the iteration range.
* @param {number|Function|Object} [opts=0] - The option object. If this is a number then it's `opts.count`. If this is a function then it's `opts.filter`.
* @param {boolean} [opts.includeComments] - The flag to iterate comments as well.
* @param {Function|null} [opts.filter=null] - The predicate function to choose tokens.
* @param {number} [opts.count=0] - The maximum count of tokens the iterator iterates. Zero is no iteration for backward compatibility.
* @returns {IterableIterator<(Token|Comment)>} The created iterator.
* @private
*/
function iterateWithCount(iteration, tokens, comments, imap, start, end, opts) {
let includeComments = false;
let count = 0;
let predicate = null;
if (typeof opts === "number") {
count = opts | 0;
} else if (typeof opts === "function") {
count = -1;
predicate = opts;
} else if (opts) {
includeComments = !!opts.includeComments;
count = (opts.count | 0) || -1;
predicate = opts.filter || null;
}
return iterateWith(iteration, tokens, comments, imap, start, end, includeComments, predicate, 0, count);
}
/**
* Iterates tokens with padding options.
*
* @param {Token[]} tokens - The array of tokens.
* @param {Comment[]} comments - The array of comments.
* @param {Object} imap - The map from locations to indices in `this.tokens`.
* @param {number} start - The start location of the iteration range.
* @param {number} end - The end location of the iteration range.
* @param {Function|Object} opts - The option object. If this is a function then it's `opts.filter`.
* @param {boolean} [opts.includeComments] - The flag to iterate comments as well.
* @param {Function|null} [opts.filter=null] - The predicate function to choose tokens.
* @param {number} [opts.count=0] - The maximum count of tokens the iterator iterates. Zero is no iteration for backward compatibility.
* @returns {IterableIterator<(Token|Comment)>} The created iterator.
* @private
*/
/**
* Iterates tokens with padding options.
*
* @param {Token[]} tokens - The array of tokens.
* @param {Comment[]} comments - The array of comments.
* @param {Object} imap - The map from locations to indices in `this.tokens`.
* @param {number} start - The start location of the iteration range.
* @param {number} end - The end location of the iteration range.
* @param {number} [beforeCount=0] - The number of tokens before the node to retrieve.
* @param {boolean} [afterCount=0] - The number of tokens after the node to retrieve.
* @returns {IterableIterator<(Token|Comment)>} The created iterator.
* @private
*/
function iterateWithPadding(tokens, comments, imap, start, end, beforeCount, afterCount) {
if (typeof beforeCount === "undefined" && typeof afterCount === "undefined") {
return iterationFwd.tokens(tokens, comments, imap, start, end);
}
if (typeof beforeCount === "number" || typeof beforeCount === "undefined") {
return iterateTokensPad(tokens, comments, imap, start, end, beforeCount | 0, afterCount | 0);
}
return iterateWithCount(iterationFwd, tokens, comments, imap, start, end, beforeCount);
}
function getFirstItem(iterable) {
for (const token of iterable) {
return token;
}
return null;
}
class IndexMap {
constructor(tokens, comments) {
this.lastIndex = tokens.length - 1;
const map = this.map = Object.create(null);
let t = 0;
let c = 0;
let border = 0;
let range = null;
while (t < tokens.length || c < comments.length) {
border = getStartOffset(comments, c);
while (t < tokens.length && (range = tokens[t].range)[0] < border) {
map[range[0]] = t;
map[range[1] - 1] = t;
t += 1;
}
border = getStartOffset(tokens, t);
while (c < comments.length && (range = comments[c].range)[0] < border) {
map[range[0]] = t;
map[range[1] - 1] = t;
c += 1;
}
}
}
/**
* Gets the index of the location `offset` in `this.tokens`.
* `offset` can be the value of `node.range[1]`, so this checks about `offset - 1` as well.
*/
firstOf(offset) {
if (typeof this.map[offset] !== "undefined") {
return this.map[offset];
}
if (typeof this.map[offset - 1] !== "undefined") {
return this.map[offset - 1] + 1;
}
return 0;
}
/**
* Gets the index of the location `offset` in `this.tokens`.
* `offset` can be the value of `node.range[0]`, so this checks about `offset - 1` as well.
*/
lastOf(offset) {
if (typeof this.map[offset] !== "undefined") {
return this.map[offset] - 1;
}
if (typeof this.map[offset - 1] !== "undefined") {
return this.map[offset - 1];
}
return this.lastIndex;
}
}
class TokenStore {
/**
* Initializes this token store.
*
* ※ `comments` needs cloning for backward compatibility.
* After this initialization, ESLint removes a shebang's comment from `comments`.
* However, so far we had been concatenating 'tokens' and 'comments',
* so the shebang's comment had remained in the concatenated array.
* As the result, both `getTokenOrCommentAfter` and `getTokenOrCommentBefore`
* methods had been returning the shebang's comment.
* And some rules depends on this behavior.
*
* @param {Token[]} tokens - The array of tokens.
* @param {Comment[]} comments - The array of comments.
*/
constructor(tokens, comments) {
this.tokens = tokens;
this.comments = comments.slice(0); // Needs cloning for backward compatibility!
this.imap = new IndexMap(tokens, comments);
}
//--------------------------------------------------------------------------
// Gets single token.
//--------------------------------------------------------------------------
/**
* Gets the token starting at the specified index.
* @param {number} offset - Index of the start of the token's range.
* @returns {Token|null} The token starting at index, or null if no such token.
*/
getTokenByRangeStart(offset) {
const index = this.imap.firstOf(offset);
const token = this.tokens[index] || null;
if (token && token.range[0] === offset) {
return token;
}
return null;
}
/**
* Gets the first token of the given node.
* @param {ASTNode} node - The AST node.
* @param {number|Function|Object} [options=0] - The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.skip=0] - The count of tokens the cursor skips.
* @returns {Token|null} An object representing the token.
*/
getFirstToken(node, options) {
return getFirstItem(iterateWithSkip(
iterationFwd,
this.tokens,
this.comments,
this.imap,
node.range[0],
node.range[1],
options
));
}
/**
* Gets the last token of the given node.
* @param {ASTNode} node - The AST node.
* @param {number|Function|Object} [options=0] - The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.skip=0] - The count of tokens the cursor skips.
* @returns {Token|null} An object representing the token.
*/
getLastToken(node, options) {
return getFirstItem(iterateWithSkip(
iterationBwd,
this.tokens,
this.comments,
this.imap,
node.range[0],
node.range[1],
options
));
}
/**
* Gets the token that precedes a given node or token.
* @param {ASTNode|Token|Comment} node - The AST node or token.
* @param {number|Function|Object} [options=0] - The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.skip=0] - The count of tokens the cursor skips.
* @returns {Token|null} An object representing the token.
*/
getTokenBefore(node, options) {
return getFirstItem(iterateWithSkip(
iterationBwd,
this.tokens,
this.comments,
this.imap,
0,
node.range[0],
options
));
}
/**
* Gets the token that follows a given node or token.
* @param {ASTNode|Token|Comment} node - The AST node or token.
* @param {number|Function|Object} [options=0] - The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.skip=0] - The count of tokens the cursor skips.
* @returns {Token|null} An object representing the token.
*/
getTokenAfter(node, options) {
return getFirstItem(iterateWithSkip(
iterationFwd,
this.tokens,
this.comments,
this.imap,
node.range[1],
Number.MAX_SAFE_INTEGER,
options
));
}
/**
* Gets the first token between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left - Node before the desired token range.
* @param {ASTNode|Token|Comment} right - Node after the desired token range.
* @param {number|Function|Object} [options=0] - The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.skip=0] - The count of tokens the cursor skips.
* @returns {Token|null} An object representing the token.
*/
getFirstTokenBetween(left, right, options) {
return getFirstItem(iterateWithSkip(
iterationFwd,
this.tokens,
this.comments,
this.imap,
left.range[1],
right.range[0],
options
));
}
/**
* Gets the last token between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left Node before the desired token range.
* @param {ASTNode|Token|Comment} right Node after the desired token range.
* @param {number|Function|Object} [options=0] The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.skip=0] - The count of tokens the cursor skips.
* @returns {Token|null} Tokens between left and right.
*/
getLastTokenBetween(left, right, options) {
return getFirstItem(iterateWithSkip(
iterationBwd,
this.tokens,
this.comments,
this.imap,
left.range[1],
right.range[0],
options
));
}
/**
* Gets the token that precedes a given node or token in the token stream.
* @param {ASTNode|Token|Comment} node The AST node or token.
* @param {number} [skipCount=0] A number of tokens to skip.
* @returns {Token|null} An object representing the token.
*/
getTokenOrCommentBefore(node, skipCount) {
return this.getTokenBefore(node, { includeComments: true, skip: skipCount });
}
/**
* Gets the token that follows a given node or token in the token stream.
* @param {ASTNode|Token|Comment} node The AST node or token.
* @param {number} [skipCount=0] A number of tokens to skip.
* @returns {Token|null} An object representing the token.
*/
getTokenOrCommentAfter(node, skipCount) {
return this.getTokenAfter(node, { includeComments: true, skip: skipCount });
}
//--------------------------------------------------------------------------
// Gets multiple tokens.
//--------------------------------------------------------------------------
/**
* Gets the first `count` tokens of the given node.
* @param {ASTNode} node - The AST node.
* @param {number|Function|Object} [options=0] - The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.count=0] - The maximum count of tokens the cursor iterates.
* @returns {Token[]} Tokens.
*/
getFirstTokens(node, options) {
return Array.from(iterateWithCount(
iterationFwd,
this.tokens,
this.comments,
this.imap,
node.range[0],
node.range[1],
options
));
}
/**
* Gets the last `count` tokens of the given node.
* @param {ASTNode} node - The AST node.
* @param {number|Function|Object} [options=0] - The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.count=0] - The maximum count of tokens the cursor iterates.
* @returns {Token[]} Tokens.
*/
getLastTokens(node, options) {
return Array.from(iterateWithCount(
iterationBwd,
this.tokens,
this.comments,
this.imap,
node.range[0],
node.range[1],
options
)).reverse();
}
/**
* Gets the `count` tokens that precedes a given node or token.
* @param {ASTNode|Token|Comment} node - The AST node or token.
* @param {number|Function|Object} [options=0] - The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.count=0] - The maximum count of tokens the cursor iterates.
* @returns {Token[]} Tokens.
*/
getTokensBefore(node, options) {
return Array.from(iterateWithCount(
iterationBwd,
this.tokens,
this.comments,
this.imap,
0,
node.range[0],
options
)).reverse();
}
/**
* Gets the `count` tokens that follows a given node or token.
* @param {ASTNode|Token|Comment} node - The AST node or token.
* @param {number|Function|Object} [options=0] - The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.count=0] - The maximum count of tokens the cursor iterates.
* @returns {Token[]} Tokens.
*/
getTokensAfter(node, options) {
return Array.from(iterateWithCount(
iterationFwd,
this.tokens,
this.comments,
this.imap,
node.range[1],
Number.MAX_SAFE_INTEGER,
options
));
}
/**
* Gets the first `count` tokens between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left - Node before the desired token range.
* @param {ASTNode|Token|Comment} right - Node after the desired token range.
* @param {number|Function|Object} [options=0] - The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.count=0] - The maximum count of tokens the cursor iterates.
* @returns {Token[]} Tokens between left and right.
*/
getFirstTokensBetween(left, right, options) {
return Array.from(iterateWithCount(
iterationFwd,
this.tokens,
this.comments,
this.imap,
left.range[1],
right.range[0],
options
));
}
/**
* Gets the last `count` tokens between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left Node before the desired token range.
* @param {ASTNode|Token|Comment} right Node after the desired token range.
* @param {number|Function|Object} [options=0] The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.count=0] - The maximum count of tokens the cursor iterates.
* @returns {Token[]} Tokens between left and right.
*/
getLastTokensBetween(left, right, options) {
return Array.from(iterateWithCount(
iterationBwd,
this.tokens,
this.comments,
this.imap,
left.range[1],
right.range[0],
options
)).reverse();
}
/**
* Gets all tokens that are related to the given node.
* @param {ASTNode} node - The AST node.
* @param {Function|Object} options The option object. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.count=0] - The maximum count of tokens the cursor iterates.
* @returns {Token[]} Array of objects representing tokens.
*/
/**
* Gets all tokens that are related to the given node.
* @param {ASTNode} node - The AST node.
* @param {int} [beforeCount=0] - The number of tokens before the node to retrieve.
* @param {int} [afterCount=0] - The number of tokens after the node to retrieve.
* @returns {Token[]} Array of objects representing tokens.
*/
getTokens(node, beforeCount, afterCount) {
return Array.from(iterateWithPadding(
this.tokens,
this.comments,
this.imap,
node.range[0],
node.range[1],
beforeCount,
afterCount
));
}
/**
* Gets all of the tokens between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left Node before the desired token range.
* @param {ASTNode|Token|Comment} right Node after the desired token range.
* @param {Function|Object} options The option object. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] - The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] - The predicate function to choose tokens.
* @param {number} [options.count=0] - The maximum count of tokens the cursor iterates.
* @returns {Token[]} Tokens between left and right.
*/
/**
* Gets all of the tokens between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left Node before the desired token range.
* @param {ASTNode|Token|Comment} right Node after the desired token range.
* @param {int} [padding=0] Number of extra tokens on either side of center.
* @returns {Token[]} Tokens between left and right.
*/
getTokensBetween(left, right, padding) {
return Array.from(iterateWithPadding(
this.tokens,
this.comments,
this.imap,
left.range[1],
right.range[0],
padding,
padding
));
}
}
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
module.exports = TokenStore;
module.exports.PUBLIC_METHODS = PUBLIC_METHODS;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment