feat(FN-4134): stabilize research view layout
Stabilized the ResearchView layout by refactoring CSS and component structure, with 175 lines of fixes spanning the CSS module, component TSX, and new regression tests. Also includes a patch changeset for the release note. Fusion-Task-Id: FN-4134 Fusion-Task-Lineage: ca3ad67b-766b-4da6-be7f-c3927d718acd
This commit is contained in:
@@ -64,20 +64,62 @@ export function countPackageTestFiles(packageDir, { projectRoot = process.cwd()
|
||||
}).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand oversized packages into virtual entries that carry vitest --shard info.
|
||||
* A package whose testFileCount exceeds splitThreshold is divided into
|
||||
* ceil(testFileCount / splitThreshold) virtual entries, each with roughly
|
||||
* equal file counts and a vitestShardIndex/vitestShardCount pair.
|
||||
*
|
||||
* @param {Array<{name:string, testFileCount:number}>} packages
|
||||
* @param {number} splitThreshold - maximum weight before splitting (default: Infinity = no split)
|
||||
* @returns {Array<{name:string, weight:number, vitestShardIndex?:number, vitestShardCount?:number}>}
|
||||
*/
|
||||
export function expandVirtualPackages(packages, splitThreshold = Infinity) {
|
||||
const result = [];
|
||||
for (const pkg of packages) {
|
||||
if (pkg.testFileCount <= splitThreshold || splitThreshold <= 0) {
|
||||
result.push({ name: pkg.name, weight: pkg.testFileCount });
|
||||
continue;
|
||||
}
|
||||
const count = Math.ceil(pkg.testFileCount / splitThreshold);
|
||||
const baseWeight = Math.floor(pkg.testFileCount / count);
|
||||
const remainder = pkg.testFileCount % count;
|
||||
for (let i = 1; i <= count; i += 1) {
|
||||
const weight = baseWeight + (i <= remainder ? 1 : 0);
|
||||
result.push({
|
||||
name: pkg.name,
|
||||
weight,
|
||||
vitestShardIndex: i,
|
||||
vitestShardCount: count,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan shard assignments using greedy bin-packing.
|
||||
* Packages exceeding the average weight per shard are automatically split
|
||||
* into virtual entries that carry vitest --shard info for intra-package
|
||||
* parallelism.
|
||||
*
|
||||
* Each returned entry is { name, weight, vitestShardIndex?, vitestShardCount? }.
|
||||
* Plain entries (no shard fields) run the full package test suite.
|
||||
* Virtual entries run `vitest --shard index/count` within the package.
|
||||
*/
|
||||
export function planShardAssignments(packages, total) {
|
||||
const totalWeight = packages.reduce((sum, p) => sum + p.testFileCount, 0);
|
||||
const splitThreshold = totalWeight > 0 ? Math.ceil(totalWeight / total) : Infinity;
|
||||
const virtualPackages = expandVirtualPackages(packages, splitThreshold);
|
||||
|
||||
const shardAssignments = Array.from({ length: total }, () => []);
|
||||
const shardWeights = Array.from({ length: total }, () => 0);
|
||||
const normalized = packages
|
||||
.map((pkg) => ({
|
||||
name: pkg.name,
|
||||
weight: pkg.testFileCount,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (b.weight !== a.weight) return b.weight - a.weight;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
const sorted = [...virtualPackages].sort((a, b) => {
|
||||
if (b.weight !== a.weight) return b.weight - a.weight;
|
||||
return a.name.localeCompare(a.name);
|
||||
});
|
||||
|
||||
for (const pkg of normalized) {
|
||||
for (const entry of sorted) {
|
||||
let targetIndex = 0;
|
||||
for (let index = 1; index < total; index += 1) {
|
||||
if (shardWeights[index] < shardWeights[targetIndex]) {
|
||||
@@ -85,15 +127,15 @@ export function planShardAssignments(packages, total) {
|
||||
}
|
||||
}
|
||||
|
||||
shardAssignments[targetIndex].push(pkg.name);
|
||||
shardWeights[targetIndex] += pkg.weight;
|
||||
shardAssignments[targetIndex].push(entry);
|
||||
shardWeights[targetIndex] += entry.weight;
|
||||
}
|
||||
|
||||
return shardAssignments;
|
||||
}
|
||||
|
||||
export function selectShardPackages(packages, shard, total) {
|
||||
return planShardAssignments(packages, total)[shard - 1];
|
||||
return planShardAssignments(packages, total)[shard - 1] || [];
|
||||
}
|
||||
|
||||
export function listWorkspaceTestPackages({ projectRoot = process.cwd() } = {}) {
|
||||
@@ -106,16 +148,23 @@ export function listWorkspaceTestPackages({ projectRoot = process.cwd() } = {})
|
||||
}));
|
||||
}
|
||||
|
||||
function entryLabel(entry) {
|
||||
if (entry.vitestShardCount) {
|
||||
return `${entry.name} [${entry.vitestShardIndex}/${entry.vitestShardCount}]`;
|
||||
}
|
||||
return entry.name;
|
||||
}
|
||||
|
||||
export function main(argv = process.argv.slice(2), env = process.env) {
|
||||
const { shard, total } = parseShardArgs(argv, env);
|
||||
const shardPackages = selectShardPackages(listWorkspaceTestPackages(), shard, total);
|
||||
const shardEntries = selectShardPackages(listWorkspaceTestPackages(), shard, total);
|
||||
|
||||
if (shardPackages.length === 0) {
|
||||
if (shardEntries.length === 0) {
|
||||
console.log(`[ci-test-shard] shard ${shard}/${total} has no assigned packages; skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[ci-test-shard] shard ${shard}/${total}: ${shardPackages.join(", ")}`);
|
||||
console.log(`[ci-test-shard] shard ${shard}/${total}: ${shardEntries.map(entryLabel).join(", ")}`);
|
||||
|
||||
const { totalWorkers, concurrency } = defaultTestWorkerBudget(env);
|
||||
const shardEnv = {
|
||||
@@ -126,8 +175,27 @@ export function main(argv = process.argv.slice(2), env = process.env) {
|
||||
|
||||
run("pnpm", ["sync:fusion-skill:check"], { env: shardEnv });
|
||||
ensureTestArtifacts(process.cwd());
|
||||
const filters = shardPackages.flatMap((pkg) => ["--filter", pkg]);
|
||||
run("pnpm", [...filters, "test"], { env: shardEnv });
|
||||
|
||||
// Group entries: plain packages run together in one pnpm invocation;
|
||||
// virtual (sharded) entries each get their own vitest --shard invocation.
|
||||
const plain = shardEntries.filter((e) => !e.vitestShardCount);
|
||||
const virtual = shardEntries.filter((e) => e.vitestShardCount);
|
||||
|
||||
if (plain.length > 0) {
|
||||
const filters = plain.flatMap((e) => ["--filter", e.name]);
|
||||
run("pnpm", [...filters, "test"], { env: shardEnv });
|
||||
}
|
||||
|
||||
for (const entry of virtual) {
|
||||
console.log(
|
||||
`[ci-test-shard] running ${entry.name} --shard ${entry.vitestShardIndex}/${entry.vitestShardCount}`,
|
||||
);
|
||||
run(
|
||||
"pnpm",
|
||||
["--filter", entry.name, "exec", "vitest", "run", "--shard", `${entry.vitestShardIndex}/${entry.vitestShardCount}`],
|
||||
{ env: shardEnv },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const currentFilePath = fileURLToPath(import.meta.url);
|
||||
|
||||
Reference in New Issue
Block a user