diff options
Diffstat (limited to 'node_modules/enhanced-resolve/lib/util')
-rw-r--r-- | node_modules/enhanced-resolve/lib/util/entrypoints.js | 667 | ||||
-rw-r--r-- | node_modules/enhanced-resolve/lib/util/identifier.js | 26 | ||||
-rw-r--r-- | node_modules/enhanced-resolve/lib/util/path.js | 221 | ||||
-rw-r--r-- | node_modules/enhanced-resolve/lib/util/process-browser.js | 16 |
4 files changed, 930 insertions, 0 deletions
diff --git a/node_modules/enhanced-resolve/lib/util/entrypoints.js b/node_modules/enhanced-resolve/lib/util/entrypoints.js new file mode 100644 index 0000000..b925d62 --- /dev/null +++ b/node_modules/enhanced-resolve/lib/util/entrypoints.js @@ -0,0 +1,667 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Ivan Kopeykin @vankop +*/ + +"use strict"; + +/** @typedef {string|(string|ConditionalMapping)[]} DirectMapping */ +/** @typedef {{[k: string]: MappingValue}} ConditionalMapping */ +/** @typedef {ConditionalMapping|DirectMapping|null} MappingValue */ +/** @typedef {Record<string, MappingValue>|ConditionalMapping|DirectMapping} ExportsField */ +/** @typedef {Record<string, MappingValue>} ImportsField */ + +/** + * @typedef {Object} PathTreeNode + * @property {Map<string, PathTreeNode>|null} children + * @property {MappingValue} folder + * @property {Map<string, MappingValue>|null} wildcards + * @property {Map<string, MappingValue>} files + */ + +/** + * Processing exports/imports field + * @callback FieldProcessor + * @param {string} request request + * @param {Set<string>} conditionNames condition names + * @returns {string[]} resolved paths + */ + +/* +Example exports field: +{ + ".": "./main.js", + "./feature": { + "browser": "./feature-browser.js", + "default": "./feature.js" + } +} +Terminology: + +Enhanced-resolve name keys ("." and "./feature") as exports field keys. + +If value is string or string[], mapping is called as a direct mapping +and value called as a direct export. + +If value is key-value object, mapping is called as a conditional mapping +and value called as a conditional export. + +Key in conditional mapping is called condition name. + +Conditional mapping nested in another conditional mapping is called nested mapping. + +---------- + +Example imports field: +{ + "#a": "./main.js", + "#moment": { + "browser": "./moment/index.js", + "default": "moment" + }, + "#moment/": { + "browser": "./moment/", + "default": "moment/" + } +} +Terminology: + +Enhanced-resolve name keys ("#a" and "#moment/", "#moment") as imports field keys. + +If value is string or string[], mapping is called as a direct mapping +and value called as a direct export. + +If value is key-value object, mapping is called as a conditional mapping +and value called as a conditional export. + +Key in conditional mapping is called condition name. + +Conditional mapping nested in another conditional mapping is called nested mapping. + +*/ + +const slashCode = "/".charCodeAt(0); +const dotCode = ".".charCodeAt(0); +const hashCode = "#".charCodeAt(0); + +/** + * @param {ExportsField} exportsField the exports field + * @returns {FieldProcessor} process callback + */ +module.exports.processExportsField = function processExportsField( + exportsField +) { + return createFieldProcessor( + buildExportsFieldPathTree(exportsField), + assertExportsFieldRequest, + assertExportTarget + ); +}; + +/** + * @param {ImportsField} importsField the exports field + * @returns {FieldProcessor} process callback + */ +module.exports.processImportsField = function processImportsField( + importsField +) { + return createFieldProcessor( + buildImportsFieldPathTree(importsField), + assertImportsFieldRequest, + assertImportTarget + ); +}; + +/** + * @param {PathTreeNode} treeRoot root + * @param {(s: string) => string} assertRequest assertRequest + * @param {(s: string, f: boolean) => void} assertTarget assertTarget + * @returns {FieldProcessor} field processor + */ +function createFieldProcessor(treeRoot, assertRequest, assertTarget) { + return function fieldProcessor(request, conditionNames) { + request = assertRequest(request); + + const match = findMatch(request, treeRoot); + + if (match === null) return []; + + const [mapping, remainRequestIndex] = match; + + /** @type {DirectMapping|null} */ + let direct = null; + + if (isConditionalMapping(mapping)) { + direct = conditionalMapping( + /** @type {ConditionalMapping} */ (mapping), + conditionNames + ); + + // matching not found + if (direct === null) return []; + } else { + direct = /** @type {DirectMapping} */ (mapping); + } + + const remainingRequest = + remainRequestIndex === request.length + 1 + ? undefined + : remainRequestIndex < 0 + ? request.slice(-remainRequestIndex - 1) + : request.slice(remainRequestIndex); + + return directMapping( + remainingRequest, + remainRequestIndex < 0, + direct, + conditionNames, + assertTarget + ); + }; +} + +/** + * @param {string} request request + * @returns {string} updated request + */ +function assertExportsFieldRequest(request) { + if (request.charCodeAt(0) !== dotCode) { + throw new Error('Request should be relative path and start with "."'); + } + if (request.length === 1) return ""; + if (request.charCodeAt(1) !== slashCode) { + throw new Error('Request should be relative path and start with "./"'); + } + if (request.charCodeAt(request.length - 1) === slashCode) { + throw new Error("Only requesting file allowed"); + } + + return request.slice(2); +} + +/** + * @param {string} request request + * @returns {string} updated request + */ +function assertImportsFieldRequest(request) { + if (request.charCodeAt(0) !== hashCode) { + throw new Error('Request should start with "#"'); + } + if (request.length === 1) { + throw new Error("Request should have at least 2 characters"); + } + if (request.charCodeAt(1) === slashCode) { + throw new Error('Request should not start with "#/"'); + } + if (request.charCodeAt(request.length - 1) === slashCode) { + throw new Error("Only requesting file allowed"); + } + + return request.slice(1); +} + +/** + * @param {string} exp export target + * @param {boolean} expectFolder is folder expected + */ +function assertExportTarget(exp, expectFolder) { + if ( + exp.charCodeAt(0) === slashCode || + (exp.charCodeAt(0) === dotCode && exp.charCodeAt(1) !== slashCode) + ) { + throw new Error( + `Export should be relative path and start with "./", got ${JSON.stringify( + exp + )}.` + ); + } + + const isFolder = exp.charCodeAt(exp.length - 1) === slashCode; + + if (isFolder !== expectFolder) { + throw new Error( + expectFolder + ? `Expecting folder to folder mapping. ${JSON.stringify( + exp + )} should end with "/"` + : `Expecting file to file mapping. ${JSON.stringify( + exp + )} should not end with "/"` + ); + } +} + +/** + * @param {string} imp import target + * @param {boolean} expectFolder is folder expected + */ +function assertImportTarget(imp, expectFolder) { + const isFolder = imp.charCodeAt(imp.length - 1) === slashCode; + + if (isFolder !== expectFolder) { + throw new Error( + expectFolder + ? `Expecting folder to folder mapping. ${JSON.stringify( + imp + )} should end with "/"` + : `Expecting file to file mapping. ${JSON.stringify( + imp + )} should not end with "/"` + ); + } +} + +/** + * Trying to match request to field + * @param {string} request request + * @param {PathTreeNode} treeRoot path tree root + * @returns {[MappingValue, number]|null} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings + */ +function findMatch(request, treeRoot) { + if (request.length === 0) { + const value = treeRoot.files.get(""); + + return value ? [value, 1] : null; + } + + if ( + treeRoot.children === null && + treeRoot.folder === null && + treeRoot.wildcards === null + ) { + const value = treeRoot.files.get(request); + + return value ? [value, request.length + 1] : null; + } + + let node = treeRoot; + let lastNonSlashIndex = 0; + let slashIndex = request.indexOf("/", 0); + + /** @type {[MappingValue, number]|null} */ + let lastFolderMatch = null; + + const applyFolderMapping = () => { + const folderMapping = node.folder; + if (folderMapping) { + if (lastFolderMatch) { + lastFolderMatch[0] = folderMapping; + lastFolderMatch[1] = -lastNonSlashIndex - 1; + } else { + lastFolderMatch = [folderMapping, -lastNonSlashIndex - 1]; + } + } + }; + + const applyWildcardMappings = (wildcardMappings, remainingRequest) => { + if (wildcardMappings) { + for (const [key, target] of wildcardMappings) { + if (remainingRequest.startsWith(key)) { + if (!lastFolderMatch) { + lastFolderMatch = [target, lastNonSlashIndex + key.length]; + } else if (lastFolderMatch[1] < lastNonSlashIndex + key.length) { + lastFolderMatch[0] = target; + lastFolderMatch[1] = lastNonSlashIndex + key.length; + } + } + } + } + }; + + while (slashIndex !== -1) { + applyFolderMapping(); + + const wildcardMappings = node.wildcards; + + if (!wildcardMappings && node.children === null) return lastFolderMatch; + + const folder = request.slice(lastNonSlashIndex, slashIndex); + + applyWildcardMappings(wildcardMappings, folder); + + if (node.children === null) return lastFolderMatch; + + const newNode = node.children.get(folder); + + if (!newNode) { + return lastFolderMatch; + } + + node = newNode; + lastNonSlashIndex = slashIndex + 1; + slashIndex = request.indexOf("/", lastNonSlashIndex); + } + + const remainingRequest = + lastNonSlashIndex > 0 ? request.slice(lastNonSlashIndex) : request; + + const value = node.files.get(remainingRequest); + + if (value) { + return [value, request.length + 1]; + } + + applyFolderMapping(); + + applyWildcardMappings(node.wildcards, remainingRequest); + + return lastFolderMatch; +} + +/** + * @param {ConditionalMapping|DirectMapping|null} mapping mapping + * @returns {boolean} is conditional mapping + */ +function isConditionalMapping(mapping) { + return ( + mapping !== null && typeof mapping === "object" && !Array.isArray(mapping) + ); +} + +/** + * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings + * @param {boolean} subpathMapping true, for subpath mappings + * @param {DirectMapping|null} mappingTarget direct export + * @param {Set<string>} conditionNames condition names + * @param {(d: string, f: boolean) => void} assert asserting direct value + * @returns {string[]} mapping result + */ +function directMapping( + remainingRequest, + subpathMapping, + mappingTarget, + conditionNames, + assert +) { + if (mappingTarget === null) return []; + + if (typeof mappingTarget === "string") { + return [ + targetMapping(remainingRequest, subpathMapping, mappingTarget, assert) + ]; + } + + const targets = []; + + for (const exp of mappingTarget) { + if (typeof exp === "string") { + targets.push( + targetMapping(remainingRequest, subpathMapping, exp, assert) + ); + continue; + } + + const mapping = conditionalMapping(exp, conditionNames); + if (!mapping) continue; + const innerExports = directMapping( + remainingRequest, + subpathMapping, + mapping, + conditionNames, + assert + ); + for (const innerExport of innerExports) { + targets.push(innerExport); + } + } + + return targets; +} + +/** + * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings + * @param {boolean} subpathMapping true, for subpath mappings + * @param {string} mappingTarget direct export + * @param {(d: string, f: boolean) => void} assert asserting direct value + * @returns {string} mapping result + */ +function targetMapping( + remainingRequest, + subpathMapping, + mappingTarget, + assert +) { + if (remainingRequest === undefined) { + assert(mappingTarget, false); + return mappingTarget; + } + if (subpathMapping) { + assert(mappingTarget, true); + return mappingTarget + remainingRequest; + } + assert(mappingTarget, false); + return mappingTarget.replace(/\*/g, remainingRequest.replace(/\$/g, "$$")); +} + +/** + * @param {ConditionalMapping} conditionalMapping_ conditional mapping + * @param {Set<string>} conditionNames condition names + * @returns {DirectMapping|null} direct mapping if found + */ +function conditionalMapping(conditionalMapping_, conditionNames) { + /** @type {[ConditionalMapping, string[], number][]} */ + let lookup = [[conditionalMapping_, Object.keys(conditionalMapping_), 0]]; + + loop: while (lookup.length > 0) { + const [mapping, conditions, j] = lookup[lookup.length - 1]; + const last = conditions.length - 1; + + for (let i = j; i < conditions.length; i++) { + const condition = conditions[i]; + + // assert default. Could be last only + if (i !== last) { + if (condition === "default") { + throw new Error("Default condition should be last one"); + } + } else if (condition === "default") { + const innerMapping = mapping[condition]; + // is nested + if (isConditionalMapping(innerMapping)) { + const conditionalMapping = /** @type {ConditionalMapping} */ (innerMapping); + lookup[lookup.length - 1][2] = i + 1; + lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]); + continue loop; + } + + return /** @type {DirectMapping} */ (innerMapping); + } + + if (conditionNames.has(condition)) { + const innerMapping = mapping[condition]; + // is nested + if (isConditionalMapping(innerMapping)) { + const conditionalMapping = /** @type {ConditionalMapping} */ (innerMapping); + lookup[lookup.length - 1][2] = i + 1; + lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]); + continue loop; + } + + return /** @type {DirectMapping} */ (innerMapping); + } + } + + lookup.pop(); + } + + return null; +} + +/** + * Internal helper to create path tree node + * to ensure that each node gets the same hidden class + * @returns {PathTreeNode} node + */ +function createNode() { + return { + children: null, + folder: null, + wildcards: null, + files: new Map() + }; +} + +/** + * Internal helper for building path tree + * @param {PathTreeNode} root root + * @param {string} path path + * @param {MappingValue} target target + */ +function walkPath(root, path, target) { + if (path.length === 0) { + root.folder = target; + return; + } + + let node = root; + // Typical path tree can looks like + // root + // - files: ["a.js", "b.js"] + // - children: + // node1: + // - files: ["a.js", "b.js"] + let lastNonSlashIndex = 0; + let slashIndex = path.indexOf("/", 0); + + while (slashIndex !== -1) { + const folder = path.slice(lastNonSlashIndex, slashIndex); + let newNode; + + if (node.children === null) { + newNode = createNode(); + node.children = new Map(); + node.children.set(folder, newNode); + } else { + newNode = node.children.get(folder); + + if (!newNode) { + newNode = createNode(); + node.children.set(folder, newNode); + } + } + + node = newNode; + lastNonSlashIndex = slashIndex + 1; + slashIndex = path.indexOf("/", lastNonSlashIndex); + } + + if (lastNonSlashIndex >= path.length) { + node.folder = target; + } else { + const file = lastNonSlashIndex > 0 ? path.slice(lastNonSlashIndex) : path; + if (file.endsWith("*")) { + if (node.wildcards === null) node.wildcards = new Map(); + node.wildcards.set(file.slice(0, -1), target); + } else { + node.files.set(file, target); + } + } +} + +/** + * @param {ExportsField} field exports field + * @returns {PathTreeNode} tree root + */ +function buildExportsFieldPathTree(field) { + const root = createNode(); + + // handle syntax sugar, if exports field is direct mapping for "." + if (typeof field === "string") { + root.files.set("", field); + + return root; + } else if (Array.isArray(field)) { + root.files.set("", field.slice()); + + return root; + } + + const keys = Object.keys(field); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + + if (key.charCodeAt(0) !== dotCode) { + // handle syntax sugar, if exports field is conditional mapping for "." + if (i === 0) { + while (i < keys.length) { + const charCode = keys[i].charCodeAt(0); + if (charCode === dotCode || charCode === slashCode) { + throw new Error( + `Exports field key should be relative path and start with "." (key: ${JSON.stringify( + key + )})` + ); + } + i++; + } + + root.files.set("", field); + return root; + } + + throw new Error( + `Exports field key should be relative path and start with "." (key: ${JSON.stringify( + key + )})` + ); + } + + if (key.length === 1) { + root.files.set("", field[key]); + continue; + } + + if (key.charCodeAt(1) !== slashCode) { + throw new Error( + `Exports field key should be relative path and start with "./" (key: ${JSON.stringify( + key + )})` + ); + } + + walkPath(root, key.slice(2), field[key]); + } + + return root; +} + +/** + * @param {ImportsField} field imports field + * @returns {PathTreeNode} root + */ +function buildImportsFieldPathTree(field) { + const root = createNode(); + + const keys = Object.keys(field); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + + if (key.charCodeAt(0) !== hashCode) { + throw new Error( + `Imports field key should start with "#" (key: ${JSON.stringify(key)})` + ); + } + + if (key.length === 1) { + throw new Error( + `Imports field key should have at least 2 characters (key: ${JSON.stringify( + key + )})` + ); + } + + if (key.charCodeAt(1) === slashCode) { + throw new Error( + `Imports field key should not start with "#/" (key: ${JSON.stringify( + key + )})` + ); + } + + walkPath(root, key.slice(1), field[key]); + } + + return root; +} diff --git a/node_modules/enhanced-resolve/lib/util/identifier.js b/node_modules/enhanced-resolve/lib/util/identifier.js new file mode 100644 index 0000000..0c29ae4 --- /dev/null +++ b/node_modules/enhanced-resolve/lib/util/identifier.js @@ -0,0 +1,26 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Ivan Kopeykin @vankop +*/ + +"use strict"; + +const PATH_QUERY_FRAGMENT_REGEXP = /^(#?(?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/; + +/** + * @param {string} identifier identifier + * @returns {[string, string, string]|null} parsed identifier + */ +function parseIdentifier(identifier) { + const match = PATH_QUERY_FRAGMENT_REGEXP.exec(identifier); + + if (!match) return null; + + return [ + match[1].replace(/\0(.)/g, "$1"), + match[2] ? match[2].replace(/\0(.)/g, "$1") : "", + match[3] || "" + ]; +} + +module.exports.parseIdentifier = parseIdentifier; diff --git a/node_modules/enhanced-resolve/lib/util/path.js b/node_modules/enhanced-resolve/lib/util/path.js new file mode 100644 index 0000000..b84c146 --- /dev/null +++ b/node_modules/enhanced-resolve/lib/util/path.js @@ -0,0 +1,221 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const path = require("path"); + +const CHAR_HASH = "#".charCodeAt(0); +const CHAR_SLASH = "/".charCodeAt(0); +const CHAR_BACKSLASH = "\\".charCodeAt(0); +const CHAR_A = "A".charCodeAt(0); +const CHAR_Z = "Z".charCodeAt(0); +const CHAR_LOWER_A = "a".charCodeAt(0); +const CHAR_LOWER_Z = "z".charCodeAt(0); +const CHAR_DOT = ".".charCodeAt(0); +const CHAR_COLON = ":".charCodeAt(0); + +const posixNormalize = path.posix.normalize; +const winNormalize = path.win32.normalize; + +/** + * @enum {number} + */ +const PathType = Object.freeze({ + Empty: 0, + Normal: 1, + Relative: 2, + AbsoluteWin: 3, + AbsolutePosix: 4, + Internal: 5 +}); +exports.PathType = PathType; + +/** + * @param {string} p a path + * @returns {PathType} type of path + */ +const getType = p => { + switch (p.length) { + case 0: + return PathType.Empty; + case 1: { + const c0 = p.charCodeAt(0); + switch (c0) { + case CHAR_DOT: + return PathType.Relative; + case CHAR_SLASH: + return PathType.AbsolutePosix; + case CHAR_HASH: + return PathType.Internal; + } + return PathType.Normal; + } + case 2: { + const c0 = p.charCodeAt(0); + switch (c0) { + case CHAR_DOT: { + const c1 = p.charCodeAt(1); + switch (c1) { + case CHAR_DOT: + case CHAR_SLASH: + return PathType.Relative; + } + return PathType.Normal; + } + case CHAR_SLASH: + return PathType.AbsolutePosix; + case CHAR_HASH: + return PathType.Internal; + } + const c1 = p.charCodeAt(1); + if (c1 === CHAR_COLON) { + if ( + (c0 >= CHAR_A && c0 <= CHAR_Z) || + (c0 >= CHAR_LOWER_A && c0 <= CHAR_LOWER_Z) + ) { + return PathType.AbsoluteWin; + } + } + return PathType.Normal; + } + } + const c0 = p.charCodeAt(0); + switch (c0) { + case CHAR_DOT: { + const c1 = p.charCodeAt(1); + switch (c1) { + case CHAR_SLASH: + return PathType.Relative; + case CHAR_DOT: { + const c2 = p.charCodeAt(2); + if (c2 === CHAR_SLASH) return PathType.Relative; + return PathType.Normal; + } + } + return PathType.Normal; + } + case CHAR_SLASH: + return PathType.AbsolutePosix; + case CHAR_HASH: + return PathType.Internal; + } + const c1 = p.charCodeAt(1); + if (c1 === CHAR_COLON) { + const c2 = p.charCodeAt(2); + if ( + (c2 === CHAR_BACKSLASH || c2 === CHAR_SLASH) && + ((c0 >= CHAR_A && c0 <= CHAR_Z) || + (c0 >= CHAR_LOWER_A && c0 <= CHAR_LOWER_Z)) + ) { + return PathType.AbsoluteWin; + } + } + return PathType.Normal; +}; +exports.getType = getType; + +/** + * @param {string} p a path + * @returns {string} the normalized path + */ +const normalize = p => { + switch (getType(p)) { + case PathType.Empty: + return p; + case PathType.AbsoluteWin: + return winNormalize(p); + case PathType.Relative: { + const r = posixNormalize(p); + return getType(r) === PathType.Relative ? r : `./${r}`; + } + } + return posixNormalize(p); +}; +exports.normalize = normalize; + +/** + * @param {string} rootPath the root path + * @param {string | undefined} request the request path + * @returns {string} the joined path + */ +const join = (rootPath, request) => { + if (!request) return normalize(rootPath); + const requestType = getType(request); + switch (requestType) { + case PathType.AbsolutePosix: + return posixNormalize(request); + case PathType.AbsoluteWin: + return winNormalize(request); + } + switch (getType(rootPath)) { + case PathType.Normal: + case PathType.Relative: + case PathType.AbsolutePosix: + return posixNormalize(`${rootPath}/${request}`); + case PathType.AbsoluteWin: + return winNormalize(`${rootPath}\\${request}`); + } + switch (requestType) { + case PathType.Empty: + return rootPath; + case PathType.Relative: { + const r = posixNormalize(rootPath); + return getType(r) === PathType.Relative ? r : `./${r}`; + } + } + return posixNormalize(rootPath); +}; +exports.join = join; + +const joinCache = new Map(); + +/** + * @param {string} rootPath the root path + * @param {string | undefined} request the request path + * @returns {string} the joined path + */ +const cachedJoin = (rootPath, request) => { + let cacheEntry; + let cache = joinCache.get(rootPath); + if (cache === undefined) { + joinCache.set(rootPath, (cache = new Map())); + } else { + cacheEntry = cache.get(request); + if (cacheEntry !== undefined) return cacheEntry; + } + cacheEntry = join(rootPath, request); + cache.set(request, cacheEntry); + return cacheEntry; +}; +exports.cachedJoin = cachedJoin; + +const checkExportsFieldTarget = relativePath => { + let lastNonSlashIndex = 2; + let slashIndex = relativePath.indexOf("/", 2); + let cd = 0; + + while (slashIndex !== -1) { + const folder = relativePath.slice(lastNonSlashIndex, slashIndex); + + switch (folder) { + case "..": { + cd--; + if (cd < 0) + return new Error( + `Trying to access out of package scope. Requesting ${relativePath}` + ); + break; + } + default: + cd++; + break; + } + + lastNonSlashIndex = slashIndex + 1; + slashIndex = relativePath.indexOf("/", lastNonSlashIndex); + } +}; +exports.checkExportsFieldTarget = checkExportsFieldTarget; diff --git a/node_modules/enhanced-resolve/lib/util/process-browser.js b/node_modules/enhanced-resolve/lib/util/process-browser.js new file mode 100644 index 0000000..277f729 --- /dev/null +++ b/node_modules/enhanced-resolve/lib/util/process-browser.js @@ -0,0 +1,16 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +module.exports = { + versions: {}, + nextTick(fn) { + const args = Array.prototype.slice.call(arguments, 1); + Promise.resolve().then(function () { + fn.apply(null, args); + }); + } +}; |