webpack/lib/util/smartGrouping.js

222 lines
5.4 KiB
JavaScript
Raw Normal View History

2020-08-27 15:59:12 +08:00
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
/**
2024-06-11 21:09:50 +08:00
* @typedef {object} GroupOptions
2020-08-27 15:59:12 +08:00
* @property {boolean=} groupChildren
* @property {boolean=} force
2020-09-02 00:08:09 +08:00
* @property {number=} targetGroupCount
2020-08-27 15:59:12 +08:00
*/
/**
2025-09-02 22:12:40 +08:00
* @template I
* @template G
2024-06-11 21:09:50 +08:00
* @typedef {object} GroupConfig
2025-09-02 22:12:40 +08:00
* @property {(item: I) => string[] | undefined} getKeys
* @property {(name: string, items: I[]) => GroupOptions=} getOptions
* @property {(key: string, children: I[], items: I[]) => G} createGroup
2020-08-27 15:59:12 +08:00
*/
2025-08-28 18:34:30 +08:00
/**
2025-09-02 22:12:40 +08:00
* @template I
* @template G
* @typedef {{ config: GroupConfig<I, G>, name: string, alreadyGrouped: boolean, items: Items<I, G> | undefined }} Group
2025-08-28 18:34:30 +08:00
*/
/**
2025-09-02 22:12:40 +08:00
* @template I, G
* @typedef {Set<Group<I, G>>} Groups
2025-08-28 18:34:30 +08:00
*/
2020-08-27 15:59:12 +08:00
/**
2025-09-02 22:12:40 +08:00
* @template I
* @template G
2024-06-11 21:09:50 +08:00
* @typedef {object} ItemWithGroups
2025-09-02 22:12:40 +08:00
* @property {I} item
* @property {Groups<I, G>} groups
2021-04-23 19:47:18 +08:00
*/
/**
2025-09-02 22:12:40 +08:00
* @template T, G
* @typedef {Set<ItemWithGroups<T, G>>} Items
2020-08-27 15:59:12 +08:00
*/
/**
2025-09-02 22:12:40 +08:00
* @template I
* @template G
2020-08-27 15:59:12 +08:00
* @template R
2025-09-02 22:12:40 +08:00
* @param {I[]} items the list of items
* @param {GroupConfig<I, G>[]} groupConfigs configuration
* @returns {(I | G)[]} grouped items
2020-08-27 15:59:12 +08:00
*/
const smartGrouping = (items, groupConfigs) => {
2025-09-02 22:12:40 +08:00
/** @type {Items<I, G>} */
2020-08-27 15:59:12 +08:00
const itemsWithGroups = new Set();
2025-09-02 22:12:40 +08:00
/** @type {Map<string, Group<I, G>>} */
2021-04-23 19:47:18 +08:00
const allGroups = new Map();
2020-08-27 15:59:12 +08:00
for (const item of items) {
2025-09-02 22:12:40 +08:00
/** @type {Groups<I, G>} */
2020-08-27 15:59:12 +08:00
const groups = new Set();
for (let i = 0; i < groupConfigs.length; i++) {
const groupConfig = groupConfigs[i];
const keys = groupConfig.getKeys(item);
if (keys) {
2021-04-23 19:47:18 +08:00
for (const name of keys) {
const key = `${i}:${name}`;
let group = allGroups.get(key);
if (group === undefined) {
allGroups.set(
key,
(group = {
config: groupConfig,
name,
alreadyGrouped: false,
items: undefined
})
);
2020-08-27 15:59:12 +08:00
}
2021-04-23 19:47:18 +08:00
groups.add(group);
2020-08-27 15:59:12 +08:00
}
}
}
itemsWithGroups.add({
item,
groups
});
}
2025-09-02 22:12:40 +08:00
2020-08-27 15:59:12 +08:00
/**
2025-09-02 22:12:40 +08:00
* @param {Items<I, G>} itemsWithGroups input items with groups
* @returns {(I | G)[]} groups items
2020-08-27 15:59:12 +08:00
*/
const runGrouping = (itemsWithGroups) => {
2020-08-27 15:59:12 +08:00
const totalSize = itemsWithGroups.size;
for (const entry of itemsWithGroups) {
for (const group of entry.groups) {
2021-04-23 19:47:18 +08:00
if (group.alreadyGrouped) continue;
const items = group.items;
if (items === undefined) {
group.items = new Set([entry]);
2020-08-27 15:59:12 +08:00
} else {
2021-04-23 19:47:18 +08:00
items.add(entry);
2020-08-27 15:59:12 +08:00
}
}
}
2025-09-02 22:12:40 +08:00
/** @type {Map<Group<I, G>, { items: Items<I, G>, options: GroupOptions | false | undefined, used: boolean }>} */
2021-04-23 19:47:18 +08:00
const groupMap = new Map();
for (const group of allGroups.values()) {
if (group.items) {
const items = group.items;
group.items = undefined;
groupMap.set(group, {
items,
options: undefined,
used: false
});
}
}
2025-09-02 22:12:40 +08:00
/** @type {(I | G)[]} */
2020-08-27 15:59:12 +08:00
const results = [];
for (;;) {
2025-09-02 22:12:40 +08:00
/** @type {Group<I, G> | undefined} */
2024-07-31 06:15:03 +08:00
let bestGroup;
2020-08-27 15:59:12 +08:00
let bestGroupSize = -1;
2025-09-02 22:12:40 +08:00
/** @type {Items<I, G> | undefined} */
2024-07-31 06:15:03 +08:00
let bestGroupItems;
2025-09-02 22:12:40 +08:00
/** @type {GroupOptions | false | undefined} */
2024-07-31 06:15:03 +08:00
let bestGroupOptions;
2021-04-23 19:47:18 +08:00
for (const [group, state] of groupMap) {
const { items, used } = state;
let options = state.options;
if (options === undefined) {
const groupConfig = group.config;
state.options = options =
(groupConfig.getOptions &&
groupConfig.getOptions(
group.name,
Array.from(items, ({ item }) => item)
)) ||
false;
}
2020-08-27 15:59:12 +08:00
const force = options && options.force;
if (!force) {
2020-09-02 00:08:09 +08:00
if (bestGroupOptions && bestGroupOptions.force) continue;
2021-04-23 19:47:18 +08:00
if (used) continue;
2020-08-27 15:59:12 +08:00
if (items.size <= 1 || totalSize - items.size <= 1) {
continue;
}
}
2020-09-02 00:08:09 +08:00
const targetGroupCount = (options && options.targetGroupCount) || 4;
2024-07-31 04:09:42 +08:00
const sizeValue = force
2020-09-02 20:09:29 +08:00
? items.size
: Math.min(
items.size,
(totalSize * 2) / targetGroupCount +
itemsWithGroups.size -
items.size
2024-07-31 05:43:19 +08:00
);
2020-09-02 00:08:09 +08:00
if (
sizeValue > bestGroupSize ||
(force && (!bestGroupOptions || !bestGroupOptions.force))
) {
2020-08-27 15:59:12 +08:00
bestGroup = group;
bestGroupSize = sizeValue;
bestGroupItems = items;
bestGroupOptions = options;
}
}
if (bestGroup === undefined) {
break;
}
const items = new Set(bestGroupItems);
2020-09-02 00:08:09 +08:00
const options = bestGroupOptions;
const groupChildren = !options || options.groupChildren !== false;
2020-08-27 15:59:12 +08:00
for (const item of items) {
itemsWithGroups.delete(item);
// Remove all groups that items have from the map to not select them again
for (const group of item.groups) {
2021-04-23 19:47:18 +08:00
const state = groupMap.get(group);
if (state !== undefined) {
state.items.delete(item);
if (state.items.size === 0) {
groupMap.delete(group);
} else {
state.options = undefined;
if (groupChildren) {
state.used = true;
}
}
2020-09-02 00:08:09 +08:00
}
2020-08-27 15:59:12 +08:00
}
}
2020-09-02 00:08:09 +08:00
groupMap.delete(bestGroup);
2021-04-23 19:47:18 +08:00
const key = bestGroup.name;
const groupConfig = bestGroup.config;
2020-09-02 00:08:09 +08:00
const allItems = Array.from(items, ({ item }) => item);
2020-08-27 15:59:12 +08:00
2021-04-23 19:47:18 +08:00
bestGroup.alreadyGrouped = true;
2020-09-02 00:08:09 +08:00
const children = groupChildren ? runGrouping(items) : allItems;
2021-04-23 19:47:18 +08:00
bestGroup.alreadyGrouped = false;
2025-09-02 22:12:40 +08:00
results.push(
groupConfig.createGroup(key, /** @type {I[]} */ (children), allItems)
);
2020-08-27 15:59:12 +08:00
}
for (const { item } of itemsWithGroups) {
results.push(item);
}
return results;
};
return runGrouping(itemsWithGroups);
};
module.exports = smartGrouping;