Files
crawler-plugin/frontend-vue/src/shared/build-chunk-planner.ts
T

197 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 构建拆包规划器(Task 91)。
*
* 为多页应用(MPA)生成 manualChunks 配置,按页面拆分公共业务 chunk:
* - vendorRules:全局 vendor 规则(Element Plus、Vue 运行时等),所有入口共享;
* - perEntryRules:按入口隔离的页面私有 chunk(匹配需同时给出 entryName
* 未匹配入口的模块不会被误拆到其他页面 chunk);
* - 同名 chunk 的规则自动合并去重;未匹配模块返回 undefined,交由 Vite
* 默认拆包逻辑处理,不强制拆包。
*
* 有界配置:maxRules 限制 vendor 规则数量(超限拒绝计数,规则不生效);
* maxEntries 限制登记的入口数量(超出部分连同其 per-entry 规则一并跳过)。
* 校验失败(空 chunk 名、空 patterns、非法上限)fail-fast 抛错且零状态变更;
* 规则读取抛错(依赖故障)时调用失败,规划状态不被污染,恢复后可继续使用。
*/
export interface ChunkRule {
/** chunk 名,不能为空 */
chunk: string
/** 匹配模块 id 的字符串 pattern 列表,不能为空 */
patterns: string[]
}
export interface EntryChunkRule extends ChunkRule {
/** 关联的入口名,不能为空 */
entry: string
}
export interface ChunkPlannerOptions {
/** 页面入口列表;超过 maxEntries 的部分被跳过 */
entries: string[]
/** 全局 vendor 拆包规则;数量超过 maxRules 的部分被拒绝 */
vendorRules?: ChunkRule[]
/** 按入口隔离的页面 chunk 规则 */
perEntryRules?: EntryChunkRule[]
/** vendor 规则数量上限,必须为正数,默认 100 */
maxRules?: number
/** 登记的入口数量上限,必须为正数,默认 100 */
maxEntries?: number
}
export interface ChunkPlan {
entry: string
chunks: string[]
}
export interface ChunkPlannerStats {
entryCount: number
chunkCount: number
sharedChunkCount: number
unmatchedCount: number
rejectedCount: number
}
export interface ChunkPlanner {
/** 分配模块到 chunk;未匹配返回 undefined(交给 Vite 默认拆包) */
assign: (moduleId: string, entryName?: string) => string | undefined
/** 生成 manualChunks 配置对象(chunk 名 → patterns */
manualChunksConfig: () => Record<string, string[]>
/** 每个入口实际包含的 chunk 清单(公共 vendor + 页面私有) */
planByEntry: () => ChunkPlan[]
stats: () => ChunkPlannerStats
}
export function createChunkPlanner(options: ChunkPlannerOptions): ChunkPlanner {
if (!Array.isArray(options.entries)) {
throw new Error('entries 必须是数组')
}
const maxRules = options.maxRules ?? 100
const maxEntries = options.maxEntries ?? 100
if (!(maxRules > 0)) {
throw new Error('maxRules 必须为正数: ' + maxRules)
}
if (!(maxEntries > 0)) {
throw new Error('maxEntries 必须为正数: ' + maxEntries)
}
const entries = options.entries.slice(0, maxEntries)
const entrySet = new Set(entries)
const vendorRules: Array<{ chunk: string; patterns: string[] }> = []
const perEntryRulesByEntry = new Map<string, Array<{ chunk: string; patterns: string[] }>>()
const chunkNames: string[] = []
let rejectedCount = 0
let unmatchedCount = 0
function validateRule(rule: ChunkRule) {
if (typeof rule.chunk !== 'string' || rule.chunk.length === 0) {
throw new Error('chunk 名不能为空')
}
if (!Array.isArray(rule.patterns) || rule.patterns.length === 0) {
throw new Error('patterns 必须是非空数组')
}
}
function mergePatterns(target: string[] | undefined, patterns: string[]): string[] {
const result = target ? [...target] : []
for (const pattern of patterns) {
if (!result.includes(pattern)) result.push(pattern)
}
return result
}
function addVendorRule(rule: ChunkRule) {
validateRule(rule)
if (vendorRules.length >= maxRules) {
rejectedCount += 1
return
}
const existing = vendorRules.find((item) => item.chunk === rule.chunk)
if (existing) {
existing.patterns = mergePatterns(existing.patterns, rule.patterns)
} else {
vendorRules.push({ chunk: rule.chunk, patterns: [...rule.patterns] })
chunkNames.push(rule.chunk)
}
}
function addPerEntryRule(rule: EntryChunkRule) {
if (typeof rule.entry !== 'string' || rule.entry.length === 0) {
throw new Error('entry 名不能为空')
}
validateRule(rule)
if (!entrySet.has(rule.entry)) return
const rules = perEntryRulesByEntry.get(rule.entry) ?? []
const existing = rules.find((item) => item.chunk === rule.chunk)
if (existing) {
existing.patterns = mergePatterns(existing.patterns, rule.patterns)
} else {
rules.push({ chunk: rule.chunk, patterns: [...rule.patterns] })
perEntryRulesByEntry.set(rule.entry, rules)
chunkNames.push(rule.chunk)
}
}
for (const rule of options.vendorRules ?? []) {
addVendorRule(rule)
}
for (const rule of options.perEntryRules ?? []) {
addPerEntryRule(rule)
}
function matches(patterns: string[], moduleId: string): boolean {
for (const pattern of patterns) {
if (moduleId.includes(pattern)) return true
}
return false
}
function assign(moduleId: string, entryName?: string): string | undefined {
for (const rule of vendorRules) {
if (matches(rule.patterns, moduleId)) return rule.chunk
}
if (entryName !== undefined) {
const rules = perEntryRulesByEntry.get(entryName)
if (rules) {
for (const rule of rules) {
if (matches(rule.patterns, moduleId)) return rule.chunk
}
}
}
unmatchedCount += 1
return undefined
}
function manualChunksConfig(): Record<string, string[]> {
const config: Record<string, string[]> = {}
for (const rule of vendorRules) {
config[rule.chunk] = mergePatterns(config[rule.chunk], rule.patterns).sort()
}
for (const rules of perEntryRulesByEntry.values()) {
for (const rule of rules) {
config[rule.chunk] = mergePatterns(config[rule.chunk], rule.patterns).sort()
}
}
return config
}
function planByEntry(): ChunkPlan[] {
const vendorNames = vendorRules.map((rule) => rule.chunk)
return entries.map((entry) => ({
entry,
chunks: [...vendorNames, ...(perEntryRulesByEntry.get(entry)?.map((rule) => rule.chunk) ?? [])],
}))
}
function stats(): ChunkPlannerStats {
return {
entryCount: entries.length,
chunkCount: chunkNames.length,
sharedChunkCount: vendorRules.length,
unmatchedCount,
rejectedCount,
}
}
return { assign, manualChunksConfig, planByEntry, stats }
}