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) })