/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const { compareRuntime } = require("./runtime"); /** @typedef {import("../Chunk")} Chunk */ /** @typedef {import("../Chunk").ChunkId} ChunkId */ /** @typedef {import("../ChunkGraph")} ChunkGraph */ /** @typedef {import("../ChunkGraph").ModuleId} ModuleId */ /** @typedef {import("../ChunkGroup")} ChunkGroup */ /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */ /** @typedef {import("../Dependency")} Dependency */ /** @typedef {import("../dependencies/HarmonyImportSideEffectDependency")} HarmonyImportSideEffectDependency */ /** @typedef {import("../dependencies/HarmonyImportSpecifierDependency")} HarmonyImportSpecifierDependency */ /** @typedef {import("../Module")} Module */ /** @typedef {import("../ModuleGraph")} ModuleGraph */ /** * @typedef {object} DependencySourceOrder * @property {number} main the main source order * @property {number} sub the sub source order */ /** * @template T * @typedef {(a: T, b: T) => -1 | 0 | 1} Comparator */ /** * @template {object} TArg * @template T * @typedef {(tArg: TArg, a: T, b: T) => -1 | 0 | 1} RawParameterizedComparator */ /** * @template {object} TArg * @template T * @typedef {(tArg: TArg) => Comparator} ParameterizedComparator */ /** * @template {object} TArg * @template {object} T * @param {RawParameterizedComparator} fn comparator with argument * @returns {ParameterizedComparator} comparator */ const createCachedParameterizedComparator = fn => { /** @type {WeakMap>} */ const map = new WeakMap(); return arg => { const cachedResult = map.get(/** @type {EXPECTED_OBJECT} */ (arg)); if (cachedResult !== undefined) return cachedResult; /** * @param {T} a first item * @param {T} b second item * @returns {-1|0|1} compare result */ const result = fn.bind(null, arg); map.set(/** @type {EXPECTED_OBJECT} */ (arg), result); return result; }; }; /** * @param {string | number} a first id * @param {string | number} b second id * @returns {-1 | 0 | 1} compare result */ const compareIds = (a, b) => { if (typeof a !== typeof b) { return typeof a < typeof b ? -1 : 1; } if (a < b) return -1; if (a > b) return 1; return 0; }; /** * @template T * @param {Comparator} elementComparator comparator for elements * @returns {Comparator>} comparator for iterables of elements */ const compareIterables = elementComparator => { const cacheEntry = compareIteratorsCache.get(elementComparator); if (cacheEntry !== undefined) return cacheEntry; /** * @param {Iterable} a first value * @param {Iterable} b second value * @returns {-1|0|1} compare result */ const result = (a, b) => { const aI = a[Symbol.iterator](); const bI = b[Symbol.iterator](); while (true) { const aItem = aI.next(); const bItem = bI.next(); if (aItem.done) { return bItem.done ? 0 : -1; } else if (bItem.done) { return 1; } const res = elementComparator(aItem.value, bItem.value); if (res !== 0) return res; } }; compareIteratorsCache.set(elementComparator, result); return result; }; /** * Compare two locations * @param {DependencyLocation} a A location node * @param {DependencyLocation} b A location node * @returns {-1|0|1} sorting comparator value */ const compareLocations = (a, b) => { const isObjectA = typeof a === "object" && a !== null; const isObjectB = typeof b === "object" && b !== null; if (!isObjectA || !isObjectB) { if (isObjectA) return 1; if (isObjectB) return -1; return 0; } if ("start" in a) { if ("start" in b) { const ap = a.start; const bp = b.start; if (ap.line < bp.line) return -1; if (ap.line > bp.line) return 1; if ( /** @type {number} */ (ap.column) < /** @type {number} */ (bp.column) ) { return -1; } if ( /** @type {number} */ (ap.column) > /** @type {number} */ (bp.column) ) { return 1; } } else { return -1; } } else if ("start" in b) { return 1; } if ("name" in a) { if ("name" in b) { if (a.name < b.name) return -1; if (a.name > b.name) return 1; } else { return -1; } } else if ("name" in b) { return 1; } if ("index" in a) { if ("index" in b) { if (/** @type {number} */ (a.index) < /** @type {number} */ (b.index)) { return -1; } if (/** @type {number} */ (a.index) > /** @type {number} */ (b.index)) { return 1; } } else { return -1; } } else if ("index" in b) { return 1; } return 0; }; /** * @param {ChunkGraph} chunkGraph the chunk graph * @param {Module} a module * @param {Module} b module * @returns {-1|0|1} compare result */ const compareModulesById = (chunkGraph, a, b) => compareIds( /** @type {ModuleId} */ (chunkGraph.getModuleId(a)), /** @type {ModuleId} */ (chunkGraph.getModuleId(b)) ); /** * @param {number} a number * @param {number} b number * @returns {-1|0|1} compare result */ const compareNumbers = (a, b) => { if (typeof a !== typeof b) { return typeof a < typeof b ? -1 : 1; } if (a < b) return -1; if (a > b) return 1; return 0; }; /** * @param {string} a string * @param {string} b string * @returns {-1|0|1} compare result */ const compareStringsNumeric = (a, b) => { const aLength = a.length; const bLength = b.length; let aChar = 0; let bChar = 0; let aIsDigit = false; let bIsDigit = false; let i = 0; let j = 0; while (i < aLength && j < bLength) { aChar = a.charCodeAt(i); bChar = b.charCodeAt(j); aIsDigit = aChar >= 48 && aChar <= 57; bIsDigit = bChar >= 48 && bChar <= 57; if (!aIsDigit && !bIsDigit) { if (aChar < bChar) return -1; if (aChar > bChar) return 1; i++; j++; } else if (aIsDigit && !bIsDigit) { // This segment of a is shorter than in b return 1; } else if (!aIsDigit && bIsDigit) { // This segment of b is shorter than in a return -1; } else { let aNumber = aChar - 48; let bNumber = bChar - 48; while (++i < aLength) { aChar = a.charCodeAt(i); if (aChar < 48 || aChar > 57) break; aNumber = aNumber * 10 + aChar - 48; } while (++j < bLength) { bChar = b.charCodeAt(j); if (bChar < 48 || bChar > 57) break; bNumber = bNumber * 10 + bChar - 48; } if (aNumber < bNumber) return -1; if (aNumber > bNumber) return 1; } } if (j < bLength) { // a is shorter than b bChar = b.charCodeAt(j); bIsDigit = bChar >= 48 && bChar <= 57; return bIsDigit ? -1 : 1; } if (i < aLength) { // b is shorter than a aChar = a.charCodeAt(i); aIsDigit = aChar >= 48 && aChar <= 57; return aIsDigit ? 1 : -1; } return 0; }; /** * @param {ModuleGraph} moduleGraph the module graph * @param {Module} a module * @param {Module} b module * @returns {-1|0|1} compare result */ const compareModulesByPostOrderIndexOrIdentifier = (moduleGraph, a, b) => { const cmp = compareNumbers( /** @type {number} */ (moduleGraph.getPostOrderIndex(a)), /** @type {number} */ (moduleGraph.getPostOrderIndex(b)) ); if (cmp !== 0) return cmp; return compareIds(a.identifier(), b.identifier()); }; /** * @param {ModuleGraph} moduleGraph the module graph * @param {Module} a module * @param {Module} b module * @returns {-1|0|1} compare result */ const compareModulesByPreOrderIndexOrIdentifier = (moduleGraph, a, b) => { const cmp = compareNumbers( /** @type {number} */ (moduleGraph.getPreOrderIndex(a)), /** @type {number} */ (moduleGraph.getPreOrderIndex(b)) ); if (cmp !== 0) return cmp; return compareIds(a.identifier(), b.identifier()); }; /** * @param {ChunkGraph} chunkGraph the chunk graph * @param {Module} a module * @param {Module} b module * @returns {-1|0|1} compare result */ const compareModulesByIdOrIdentifier = (chunkGraph, a, b) => { const cmp = compareIds( /** @type {ModuleId} */ (chunkGraph.getModuleId(a)), /** @type {ModuleId} */ (chunkGraph.getModuleId(b)) ); if (cmp !== 0) return cmp; return compareIds(a.identifier(), b.identifier()); }; /** * @param {ChunkGraph} chunkGraph the chunk graph * @param {Chunk} a chunk * @param {Chunk} b chunk * @returns {-1 | 0 | 1} compare result */ const compareChunks = (chunkGraph, a, b) => chunkGraph.compareChunks(a, b); /** * @param {string} a first string * @param {string} b second string * @returns {-1|0|1} compare result */ const compareStrings = (a, b) => { if (a < b) return -1; if (a > b) return 1; return 0; }; /** * @param {ChunkGroup} a first chunk group * @param {ChunkGroup} b second chunk group * @returns {-1 | 0 | 1} compare result */ const compareChunkGroupsByIndex = (a, b) => /** @type {number} */ (a.index) < /** @type {number} */ (b.index) ? -1 : 1; /** * @template {EXPECTED_OBJECT} K1 * @template {EXPECTED_OBJECT} K2 * @template T */ class TwoKeyWeakMap { constructor() { /** * @private * @type {WeakMap>} */ this._map = new WeakMap(); } /** * @param {K1} key1 first key * @param {K2} key2 second key * @returns {T | undefined} value */ get(key1, key2) { const childMap = this._map.get(key1); if (childMap === undefined) { return; } return childMap.get(key2); } /** * @param {K1} key1 first key * @param {K2} key2 second key * @param {T | undefined} value new value * @returns {void} */ set(key1, key2, value) { let childMap = this._map.get(key1); if (childMap === undefined) { childMap = new WeakMap(); this._map.set(key1, childMap); } childMap.set(key2, value); } } /** @type {TwoKeyWeakMap, Comparator, Comparator>}} */ const concatComparatorsCache = new TwoKeyWeakMap(); /** * @template T * @param {Comparator} c1 comparator * @param {Comparator} c2 comparator * @param {Comparator[]} cRest comparators * @returns {Comparator} comparator */ const concatComparators = (c1, c2, ...cRest) => { if (cRest.length > 0) { const [c3, ...cRest2] = cRest; return concatComparators(c1, concatComparators(c2, c3, ...cRest2)); } const cacheEntry = /** @type {Comparator} */ ( concatComparatorsCache.get(c1, c2) ); if (cacheEntry !== undefined) return cacheEntry; /** * @param {T} a first value * @param {T} b second value * @returns {-1|0|1} compare result */ const result = (a, b) => { const res = c1(a, b); if (res !== 0) return res; return c2(a, b); }; concatComparatorsCache.set(c1, c2, result); return result; }; /** * @template A, B * @typedef {(input: A) => B | undefined | null} Selector */ /** @type {TwoKeyWeakMap, Comparator, Comparator>}} */ const compareSelectCache = new TwoKeyWeakMap(); /** * @template T * @template R * @param {Selector} getter getter for value * @param {Comparator} comparator comparator * @returns {Comparator} comparator */ const compareSelect = (getter, comparator) => { const cacheEntry = compareSelectCache.get(getter, comparator); if (cacheEntry !== undefined) return cacheEntry; /** * @param {T} a first value * @param {T} b second value * @returns {-1|0|1} compare result */ const result = (a, b) => { const aValue = getter(a); const bValue = getter(b); if (aValue !== undefined && aValue !== null) { if (bValue !== undefined && bValue !== null) { return comparator(aValue, bValue); } return -1; } if (bValue !== undefined && bValue !== null) { return 1; } return 0; }; compareSelectCache.set(getter, comparator, result); return result; }; /** @type {WeakMap, Comparator>>} */ const compareIteratorsCache = new WeakMap(); // TODO this is no longer needed when minimum node.js version is >= 12 // since these versions ship with a stable sort function /** * @template T * @param {Iterable} iterable original ordered list * @returns {Comparator} comparator */ const keepOriginalOrder = iterable => { /** @type {Map} */ const map = new Map(); let i = 0; for (const item of iterable) { map.set(item, i++); } return (a, b) => compareNumbers( /** @type {number} */ (map.get(a)), /** @type {number} */ (map.get(b)) ); }; /** * @param {ChunkGraph} chunkGraph the chunk graph * @returns {Comparator} comparator */ const compareChunksNatural = chunkGraph => { const cmpFn = module.exports.compareModulesById(chunkGraph); const cmpIterableFn = compareIterables(cmpFn); return concatComparators( compareSelect( chunk => /** @type {string|number} */ (chunk.name), compareIds ), compareSelect(chunk => chunk.runtime, compareRuntime), compareSelect( /** * @param {Chunk} chunk a chunk * @returns {Iterable} modules */ chunk => chunkGraph.getOrderedChunkModulesIterable(chunk, cmpFn), cmpIterableFn ) ); }; /** * For HarmonyImportSideEffectDependency and HarmonyImportSpecifierDependency, we should prioritize import order to match the behavior of running modules directly in a JS engine without a bundler. * For other types like ConstDependency, we can instead prioritize usage order. * https://github.com/webpack/webpack/pull/19686 * @param {Dependency[]} dependencies dependencies * @param {WeakMap} dependencySourceOrderMap dependency source order map * @returns {void} */ const sortWithSourceOrder = (dependencies, dependencySourceOrderMap) => { /** * @param {Dependency} dep dependency * @returns {number} source order */ const getSourceOrder = dep => { if (dependencySourceOrderMap.has(dep)) { const { main } = /** @type {DependencySourceOrder} */ ( dependencySourceOrderMap.get(dep) ); return main; } return /** @type { HarmonyImportSideEffectDependency | HarmonyImportSpecifierDependency} */ ( dep ).sourceOrder; }; /** * If the sourceOrder is a number, it means the dependency needs to be sorted. * @param {number | undefined} sourceOrder sourceOrder * @returns {boolean} needReSort */ const needReSort = sourceOrder => { if (typeof sourceOrder === "number") { return true; } return false; }; // Extract dependencies with sourceOrder and sort them const withSourceOrder = []; // First pass: collect dependencies with sourceOrder for (let i = 0; i < dependencies.length; i++) { const dep = dependencies[i]; const sourceOrder = getSourceOrder(dep); if (needReSort(sourceOrder)) { withSourceOrder.push({ dep, sourceOrder, originalIndex: i }); } } if (withSourceOrder.length <= 1) { return; } // Sort dependencies with sourceOrder withSourceOrder.sort((a, b) => { // Handle both dependencies in map case if ( dependencySourceOrderMap.has(a.dep) && dependencySourceOrderMap.has(b.dep) ) { const { main: mainA, sub: subA } = /** @type {DependencySourceOrder} */ ( dependencySourceOrderMap.get(a.dep) ); const { main: mainB, sub: subB } = /** @type {DependencySourceOrder} */ ( dependencySourceOrderMap.get(b.dep) ); if (mainA === mainB) { return compareNumbers(subA, subB); } return compareNumbers(mainA, mainB); } return compareNumbers(a.sourceOrder, b.sourceOrder); }); // Second pass: build result array let sortedIndex = 0; for (let i = 0; i < dependencies.length; i++) { const dep = dependencies[i]; const sourceOrder = getSourceOrder(dep); if (needReSort(sourceOrder)) { dependencies[i] = withSourceOrder[sortedIndex].dep; sortedIndex++; } } }; module.exports.compareChunkGroupsByIndex = compareChunkGroupsByIndex; /** @type {ParameterizedComparator} */ module.exports.compareChunks = createCachedParameterizedComparator(compareChunks); /** * @param {Chunk} a chunk * @param {Chunk} b chunk * @returns {-1|0|1} compare result */ module.exports.compareChunksById = (a, b) => compareIds(/** @type {ChunkId} */ (a.id), /** @type {ChunkId} */ (b.id)); module.exports.compareChunksNatural = compareChunksNatural; module.exports.compareIds = compareIds; module.exports.compareIterables = compareIterables; module.exports.compareLocations = compareLocations; /** @type {ParameterizedComparator} */ module.exports.compareModulesById = createCachedParameterizedComparator(compareModulesById); /** @type {ParameterizedComparator} */ module.exports.compareModulesByIdOrIdentifier = createCachedParameterizedComparator(compareModulesByIdOrIdentifier); /** * @param {Module} a module * @param {Module} b module * @returns {-1|0|1} compare result */ module.exports.compareModulesByIdentifier = (a, b) => compareIds(a.identifier(), b.identifier()); /** @type {ParameterizedComparator} */ module.exports.compareModulesByPostOrderIndexOrIdentifier = createCachedParameterizedComparator( compareModulesByPostOrderIndexOrIdentifier ); /** @type {ParameterizedComparator} */ module.exports.compareModulesByPreOrderIndexOrIdentifier = createCachedParameterizedComparator( compareModulesByPreOrderIndexOrIdentifier ); module.exports.compareNumbers = compareNumbers; module.exports.compareSelect = compareSelect; module.exports.compareStrings = compareStrings; module.exports.compareStringsNumeric = compareStringsNumeric; module.exports.concatComparators = concatComparators; module.exports.keepOriginalOrder = keepOriginalOrder; module.exports.sortWithSourceOrder = sortWithSourceOrder;