1// @effect-diagnostics nodeBuiltinImport:off - vitest loads this sequencer from the config,
2// outside any Effect runtime, and only needs path.relative.
3import * as NodePath from "node:path";
4
5import { BaseSequencer, type TestSpecification } from "vite-plus/test/node";
6
7import shardWeights from "./shardWeights.json" with { type: "json" };
8
9// Use the recorded median for new files.
10const UNRECORDED_FILE_SECONDS = 0.6;
11
12const recordedSeconds: Readonly<Record<string, number>> = shardWeights;
13
14// Include import time (~1/3 of CI shards) in weights; derive identical splits so each file runs once.
15export class WeightedShardSequencer extends BaseSequencer {
16 override async shard(files: TestSpecification[]) {
17 const { index, count } = this.ctx.config.shard ?? { index: 1, count: 1 };
18 const weighted = files
19 .map((spec) => {
20 const key = NodePath.relative(this.ctx.config.root, spec.moduleId).replaceAll("\\", "/");
21 return { spec, key, seconds: recordedSeconds[key] ?? UNRECORDED_FILE_SECONDS };
22 })
23 .toSorted((a, b) => b.seconds - a.seconds || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
24
25 const loads = Array.from({ length: count }, () => 0);
26 const picked: TestSpecification[] = [];
27 for (const file of weighted) {
28 const least = Math.min(...loads);
29 const lightest = loads.indexOf(least);
30 loads[lightest] = least + file.seconds;
31 if (lightest === index - 1) picked.push(file.spec);
32 }
33 return picked;
34 }
35}