211 lines
9.1 KiB
TypeScript
211 lines
9.1 KiB
TypeScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { createChunkPlanner } from '../src/shared/build-chunk-planner.ts'
|
|
|
|
test('test_task_091_chunk_normal_default_path', () => {
|
|
const planner = createChunkPlanner({
|
|
entries: ['publish', 'dedupe', 'convert'],
|
|
vendorRules: [
|
|
{ chunk: 'element-plus', patterns: ['element-plus', '@element-plus'] },
|
|
{ chunk: 'shared', patterns: ['/src/shared/'] },
|
|
{ chunk: 'vue-vendor', patterns: ['/node_modules/vue/', '/node_modules/@vue/', 'vue-router'] },
|
|
],
|
|
perEntryRules: [
|
|
{ entry: 'publish', chunk: 'publish-page', patterns: ['/src/pages/publish/'] },
|
|
{ entry: 'dedupe', chunk: 'dedupe-page', patterns: ['/src/pages/dedupe/'] },
|
|
{ entry: 'convert', chunk: 'convert-page', patterns: ['/src/pages/convert/'] },
|
|
],
|
|
})
|
|
const config = planner.manualChunksConfig()
|
|
assert.deepEqual(
|
|
Object.keys(config).sort(),
|
|
['convert-page', 'dedupe-page', 'element-plus', 'publish-page', 'shared', 'vue-vendor'].sort(),
|
|
'默认路径产出全部计划 chunk',
|
|
)
|
|
// Element Plus 模块进入独立 chunk
|
|
assert.equal(
|
|
planner.assign('D:/repo/node_modules/element-plus/es/components/button/index.mjs', 'publish'),
|
|
'element-plus',
|
|
)
|
|
// 公共业务模块进入公共业务 chunk
|
|
assert.equal(planner.assign('D:/repo/src/shared/api/java-modules.ts', 'publish'), 'shared')
|
|
// 页面私有模块进入页面 chunk
|
|
assert.equal(planner.assign('D:/repo/src/pages/publish/components/Table.vue', 'publish'), 'publish-page')
|
|
// vue 依赖进入 vendor chunk
|
|
assert.equal(planner.assign('D:/repo/node_modules/vue/dist/vue.runtime.esm-bundler.js', 'publish'), 'vue-vendor')
|
|
// 未匹配模块不强制拆包,交给 Vite 默认处理
|
|
assert.equal(planner.assign('D:/repo/src/main.ts', 'publish'), undefined)
|
|
const stats = planner.stats()
|
|
assert.equal(stats.entryCount, 3)
|
|
assert.equal(stats.chunkCount, 6)
|
|
assert.equal(stats.sharedChunkCount, 3)
|
|
assert.equal(stats.unmatchedCount, 1)
|
|
})
|
|
|
|
test('test_task_091_chunk_normal_multiple_items', () => {
|
|
const entries = ['publish', 'dedupe', 'convert', 'split', 'delete-brand']
|
|
const planner = createChunkPlanner({
|
|
entries,
|
|
vendorRules: [{ chunk: 'element-plus', patterns: ['element-plus'] }],
|
|
perEntryRules: entries.map((entry) => ({
|
|
entry,
|
|
chunk: `${entry}-page`,
|
|
patterns: [`/src/pages/${entry}/`],
|
|
})),
|
|
})
|
|
// 批量分配:多个模块同一 chunk,结果不丢失且顺序稳定
|
|
for (let i = 0; i < 3; i++) {
|
|
assert.equal(planner.assign(`D:/repo/node_modules/element-plus/es/components/table/index.mjs`, entries[i]), 'element-plus')
|
|
}
|
|
for (const entry of entries) {
|
|
assert.equal(planner.assign(`D:/repo/src/pages/${entry}/index.vue`, entry), `${entry}-page`)
|
|
}
|
|
const plan = planner.planByEntry()
|
|
assert.equal(plan.length, 5)
|
|
assert.equal(plan[0].entry, 'publish')
|
|
assert.equal(plan[0].chunks[0], 'element-plus')
|
|
assert.equal(plan[0].chunks[1], 'publish-page')
|
|
assert.equal(plan[4].chunks[1], 'delete-brand-page')
|
|
// 每入口均含 element-plus + 自己的页面 chunk
|
|
for (const item of plan) {
|
|
assert.ok(item.chunks.includes('element-plus'))
|
|
assert.ok(item.chunks.includes(`${item.entry}-page`))
|
|
assert.equal(item.chunks.length, 2)
|
|
}
|
|
})
|
|
|
|
test('test_task_091_chunk_normal_repeated_operation_is_idempotent', () => {
|
|
const options = {
|
|
entries: ['publish', 'dedupe'],
|
|
vendorRules: [
|
|
{ chunk: 'element-plus', patterns: ['element-plus'] },
|
|
{ chunk: 'shared', patterns: ['/src/shared/'] },
|
|
],
|
|
perEntryRules: [
|
|
{ entry: 'publish', chunk: 'publish-page', patterns: ['/src/pages/publish/'] },
|
|
],
|
|
}
|
|
const planner = createChunkPlanner(options)
|
|
const once = planner.manualChunksConfig()
|
|
const twice = planner.manualChunksConfig()
|
|
assert.deepEqual(once, twice, '重复生成配置结果一致')
|
|
// 重复登记同 chunk 同名规则:合并去重,不产生重复 chunk
|
|
const merged = createChunkPlanner({
|
|
entries: ['publish', 'dedupe'],
|
|
vendorRules: [
|
|
{ chunk: 'element-plus', patterns: ['element-plus'] },
|
|
{ chunk: 'element-plus', patterns: ['@element-plus'] },
|
|
{ chunk: 'shared', patterns: ['/src/shared/'] },
|
|
],
|
|
perEntryRules: [
|
|
{ entry: 'publish', chunk: 'publish-page', patterns: ['/src/pages/publish/'] },
|
|
],
|
|
})
|
|
assert.deepEqual(merged.manualChunksConfig()['element-plus'].sort(), ['@element-plus', 'element-plus'])
|
|
assert.equal(merged.stats().chunkCount, 3)
|
|
// 重复 assign 同一模块:结果不变,命中统计幂等
|
|
assert.equal(planner.assign('x/node_modules/element-plus/a.js', 'publish'), 'element-plus')
|
|
assert.equal(planner.assign('x/node_modules/element-plus/a.js', 'publish'), 'element-plus')
|
|
assert.equal(planner.stats().unmatchedCount, 0)
|
|
})
|
|
|
|
test('test_task_091_chunk_boundary_empty_input', () => {
|
|
const planner = createChunkPlanner({ entries: [] })
|
|
assert.deepEqual(planner.manualChunksConfig(), {}, '无入口产空配置')
|
|
assert.deepEqual(planner.planByEntry(), [])
|
|
assert.equal(planner.stats().entryCount, 0)
|
|
assert.equal(planner.stats().chunkCount, 0)
|
|
assert.equal(planner.assign('x/module.js', 'publish'), undefined)
|
|
})
|
|
|
|
test('test_task_091_chunk_boundary_single_item', () => {
|
|
const planner = createChunkPlanner({
|
|
entries: ['withdraw'],
|
|
vendorRules: [{ chunk: 'element-plus', patterns: ['element-plus'] }],
|
|
perEntryRules: [{ entry: 'withdraw', chunk: 'withdraw-page', patterns: ['/src/pages/withdraw/'] }],
|
|
})
|
|
assert.equal(planner.assign('/repo/node_modules/element-plus/es/index.mjs', 'withdraw'), 'element-plus')
|
|
assert.equal(planner.assign('/repo/src/pages/withdraw/index.vue', 'withdraw'), 'withdraw-page')
|
|
assert.equal(planner.assign('/repo/src/pages/withdraw/index.vue', 'publish'), undefined, '入口不匹配时页面规则不生效')
|
|
const plan = planner.planByEntry()
|
|
assert.equal(plan.length, 1)
|
|
assert.equal(plan[0].chunks.length, 2)
|
|
})
|
|
|
|
test('test_task_091_chunk_boundary_limit_and_overflow', () => {
|
|
const planner = createChunkPlanner({
|
|
entries: ['a', 'b', 'c'],
|
|
vendorRules: [1, 2, 3, 4, 5].map((n) => ({ chunk: `vendor-${n}`, patterns: [`/vendor-${n}/`] })),
|
|
perEntryRules: [
|
|
{ entry: 'a', chunk: 'a-page', patterns: ['/src/pages/a/'] },
|
|
{ entry: 'b', chunk: 'b-page', patterns: ['/src/pages/b/'] },
|
|
{ entry: 'c', chunk: 'c-page', patterns: ['/src/pages/c/'] },
|
|
],
|
|
maxRules: 3,
|
|
maxEntries: 2,
|
|
})
|
|
const stats = planner.stats()
|
|
assert.equal(stats.chunkCount, 5, '规则超限后剩余规则被拒绝')
|
|
assert.equal(stats.rejectedCount, 2, 'vendor-4/vendor-5 被拒绝')
|
|
assert.equal(stats.entryCount, 2, '入口超过 maxEntries 只登记前 2 个')
|
|
assert.equal(planner.assign('x/vendor-1/a.js'), 'vendor-1')
|
|
assert.equal(planner.assign('x/vendor-4/a.js'), undefined, '被拒绝的规则不再生效')
|
|
const plan = planner.planByEntry()
|
|
assert.equal(plan.length, 2)
|
|
})
|
|
|
|
test('test_task_091_chunk_invalid_input_rejected', () => {
|
|
assert.throws(() => createChunkPlanner({ entries: 'publish' as never }), /entries 必须是数组/)
|
|
assert.throws(
|
|
() => createChunkPlanner({ entries: ['a'], vendorRules: [{ chunk: '', patterns: ['x'] }] }),
|
|
/chunk 名不能为空/,
|
|
)
|
|
assert.throws(
|
|
() => createChunkPlanner({ entries: ['a'], vendorRules: [{ chunk: 'v', patterns: [] }] }),
|
|
/patterns 必须是非空数组/,
|
|
)
|
|
assert.throws(
|
|
() => createChunkPlanner({ entries: ['a'], vendorRules: [{ chunk: 'v', patterns: 'x' as never }] }),
|
|
/patterns 必须是非空数组/,
|
|
)
|
|
assert.throws(() => createChunkPlanner({ entries: ['a'], maxRules: 0 }), /maxRules 必须为正数/)
|
|
assert.throws(() => createChunkPlanner({ entries: ['a'], maxEntries: -1 }), /maxEntries 必须为正数/)
|
|
assert.throws(
|
|
() => createChunkPlanner({ entries: ['a'], perEntryRules: [{ entry: '', chunk: 'x', patterns: ['y'] }] }),
|
|
/entry 名不能为空/,
|
|
)
|
|
})
|
|
|
|
test('test_task_091_chunk_dependency_failure_releases_resources', () => {
|
|
let broken = true
|
|
const poisonedPatterns = new Proxy(['element-plus'], {
|
|
get(target, prop, receiver) {
|
|
if (broken && prop === '0') throw new Error('pattern getter down')
|
|
return Reflect.get(target, prop, receiver)
|
|
},
|
|
ownKeys(target) {
|
|
if (broken) throw new Error('pattern ownKeys down')
|
|
return Reflect.ownKeys(target)
|
|
},
|
|
})
|
|
// 依赖(规则读取)失败:构造期抛错,规划器不产生部分状态
|
|
assert.throws(
|
|
() =>
|
|
createChunkPlanner({
|
|
entries: ['publish'],
|
|
vendorRules: [{ chunk: 'element-plus', patterns: poisonedPatterns as unknown as string[] }],
|
|
}),
|
|
/pattern/,
|
|
)
|
|
// 错误可恢复:依赖恢复后同一配置成功
|
|
broken = false
|
|
const planner = createChunkPlanner({
|
|
entries: ['publish'],
|
|
vendorRules: [{ chunk: 'element-plus', patterns: poisonedPatterns as unknown as string[] }],
|
|
})
|
|
assert.equal(planner.assign('/repo/node_modules/element-plus/a.js', 'publish'), 'element-plus')
|
|
assert.deepEqual(planner.manualChunksConfig()['element-plus'], ['element-plus'])
|
|
assert.equal(planner.stats().rejectedCount, 0, '失败不产生拒绝计数')
|
|
assert.equal(planner.stats().chunkCount, 1)
|
|
})
|