codemodctl 0.1.24 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +389 -0
- package/dist/codemod-cli-Bg-LwFEk.js +26 -0
- package/dist/codeowner-analysis-BAoreMb0.d.ts +95 -0
- package/dist/codeowner-analysis-pEygA0IV.js +158 -0
- package/dist/codeowners.d.ts +2 -0
- package/dist/codeowners.js +5 -0
- package/dist/consistent-sharding-CQg-qiBA.js +108 -0
- package/dist/consistent-sharding-bHfBogeY.d.ts +67 -0
- package/dist/directory-analysis-C4q0A0YM.js +98 -0
- package/dist/directory.d.ts +72 -0
- package/dist/directory.js +6 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/sharding.d.ts +2 -0
- package/dist/sharding.js +4 -0
- package/package.json +1 -1
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./codemod-cli-Bg-LwFEk.js";
|
|
3
|
+
import { t as analyzeCodeowners } from "./codeowner-analysis-pEygA0IV.js";
|
|
4
|
+
import "./consistent-sharding-CQg-qiBA.js";
|
|
5
|
+
import { t as analyzeDirectories } from "./directory-analysis-C4q0A0YM.js";
|
|
6
|
+
import { defineCommand, runMain } from "citty";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import { $ } from "execa";
|
|
9
|
+
import { writeFile } from "node:fs/promises";
|
|
10
|
+
|
|
11
|
+
//#region src/commands/git/create-pr.ts
|
|
12
|
+
const createPrCommand = defineCommand({
|
|
13
|
+
meta: {
|
|
14
|
+
name: "create-pr",
|
|
15
|
+
description: "Create a pull request"
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
title: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Title of the pull request",
|
|
21
|
+
required: true
|
|
22
|
+
},
|
|
23
|
+
body: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Body/description of the pull request",
|
|
26
|
+
required: false
|
|
27
|
+
},
|
|
28
|
+
head: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Head branch for the pull request",
|
|
31
|
+
required: false
|
|
32
|
+
},
|
|
33
|
+
base: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Base branch to merge into",
|
|
36
|
+
required: false
|
|
37
|
+
},
|
|
38
|
+
push: {
|
|
39
|
+
type: "boolean",
|
|
40
|
+
required: false
|
|
41
|
+
},
|
|
42
|
+
commitMessage: {
|
|
43
|
+
alias: "m",
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Message to commit",
|
|
46
|
+
required: false
|
|
47
|
+
},
|
|
48
|
+
branchName: {
|
|
49
|
+
alias: "b",
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Branch to create the pull request from",
|
|
52
|
+
required: false
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async run({ args }) {
|
|
56
|
+
const { title, body, head, base, push, commitMessage, branchName } = args;
|
|
57
|
+
if (push && !commitMessage) {
|
|
58
|
+
console.error("Error: commitMessage is required if commit is true");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const apiEndpoint = process.env.BUTTERFLOW_API_ENDPOINT;
|
|
62
|
+
const authToken = process.env.BUTTERFLOW_API_AUTH_TOKEN;
|
|
63
|
+
const taskId = process.env.CODEMOD_TASK_ID;
|
|
64
|
+
if (!taskId) {
|
|
65
|
+
console.error("Error: CODEMOD_TASK_ID environment variable is required");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
if (!apiEndpoint) {
|
|
69
|
+
console.error("Error: BUTTERFLOW_API_ENDPOINT environment variable is required");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
if (!authToken) {
|
|
73
|
+
console.error("Error: BUTTERFLOW_API_AUTH_TOKEN environment variable is required");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const prData = { title };
|
|
77
|
+
if (push) {
|
|
78
|
+
try {
|
|
79
|
+
await $`git diff --quiet`;
|
|
80
|
+
await $`git diff --cached --quiet`;
|
|
81
|
+
console.error("No changes detected, skipping pull request creation.");
|
|
82
|
+
process.exit(0);
|
|
83
|
+
} catch {
|
|
84
|
+
console.log("Changes detected, proceeding with pull request creation...");
|
|
85
|
+
}
|
|
86
|
+
const taskIdSignature = crypto.createHash("sha256").update(taskId).digest("hex").slice(0, 8);
|
|
87
|
+
const codemodBranchName = branchName ? branchName : `codemod-${taskIdSignature}`;
|
|
88
|
+
let remoteBaseBranch;
|
|
89
|
+
try {
|
|
90
|
+
remoteBaseBranch = (await $`git remote show origin`).stdout.match(/HEAD branch: (.+)/)?.[1]?.trim() || "main";
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error("Error: Failed to get remote base branch");
|
|
93
|
+
console.error(error);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (codemodBranchName) prData.head = codemodBranchName;
|
|
97
|
+
if (remoteBaseBranch) prData.base = remoteBaseBranch;
|
|
98
|
+
console.debug(`Remote base branch: ${remoteBaseBranch}`);
|
|
99
|
+
try {
|
|
100
|
+
await $`git checkout -b ${codemodBranchName}`;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Error: Failed to checkout branch");
|
|
103
|
+
console.error(error);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await $`git add .`;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error("Error: Failed to add changes");
|
|
110
|
+
console.error(error);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await $`git commit --no-verify -m ${commitMessage}`;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("Error: Failed to commit changes");
|
|
117
|
+
console.error(error);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
await $`git push origin ${codemodBranchName} --force`;
|
|
122
|
+
console.log(`Pushed branch to origin: ${codemodBranchName}`);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error("Error: Failed to push changes");
|
|
125
|
+
console.error(error);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (body) prData.body = body;
|
|
130
|
+
if (head && !prData.head) prData.head = head;
|
|
131
|
+
if (base && !prData.base) prData.base = base;
|
|
132
|
+
try {
|
|
133
|
+
console.debug("Creating pull request...");
|
|
134
|
+
console.debug(`Title: ${title}`);
|
|
135
|
+
if (body) console.debug(`Body: ${body}`);
|
|
136
|
+
if (head) console.debug(`Head: ${head}`);
|
|
137
|
+
console.debug(`Base: ${base}`);
|
|
138
|
+
const response = await fetch(`${apiEndpoint}/api/butterflow/v1/tasks/${taskId}/pull-request`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
Authorization: `Bearer ${authToken}`,
|
|
142
|
+
"Content-Type": "application/json"
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify(prData)
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const errorText = await response.text();
|
|
148
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
149
|
+
}
|
|
150
|
+
await response.json();
|
|
151
|
+
console.log("✅ Pull request created successfully!");
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error("❌ Failed to create pull request:");
|
|
154
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/commands/git/index.ts
|
|
162
|
+
const gitCommand = defineCommand({
|
|
163
|
+
meta: {
|
|
164
|
+
name: "git",
|
|
165
|
+
description: "Git operations"
|
|
166
|
+
},
|
|
167
|
+
subCommands: { "create-pr": createPrCommand }
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/commands/shard/codeowner.ts
|
|
172
|
+
/**
|
|
173
|
+
* Codeowner-based sharding command
|
|
174
|
+
*
|
|
175
|
+
* Creates shards by grouping files by their CODEOWNERS team assignments.
|
|
176
|
+
* Uses simple file distribution within each team group, maintaining
|
|
177
|
+
* consistency with existing state when available.
|
|
178
|
+
*
|
|
179
|
+
* Example usage:
|
|
180
|
+
* npx codemodctl shard codeowner -l tsx -c ./codemod.ts -s 30 --stateProp shards --codeowners .github/CODEOWNERS
|
|
181
|
+
*
|
|
182
|
+
* This will analyze all applicable files, group them by CODEOWNERS team assignments, and create
|
|
183
|
+
* shards with approximately 30 files each within each team.
|
|
184
|
+
*/
|
|
185
|
+
const codeownerCommand = defineCommand({
|
|
186
|
+
meta: {
|
|
187
|
+
name: "codeowner",
|
|
188
|
+
description: "Analyze GitHub CODEOWNERS file and create sharding output"
|
|
189
|
+
},
|
|
190
|
+
args: {
|
|
191
|
+
shardSize: {
|
|
192
|
+
type: "string",
|
|
193
|
+
alias: "s",
|
|
194
|
+
description: "Number of files per shard",
|
|
195
|
+
required: true
|
|
196
|
+
},
|
|
197
|
+
stateProp: {
|
|
198
|
+
type: "string",
|
|
199
|
+
alias: "p",
|
|
200
|
+
description: "Property name for state output",
|
|
201
|
+
required: true
|
|
202
|
+
},
|
|
203
|
+
codeowners: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "Path to CODEOWNERS file (optional)",
|
|
206
|
+
required: false
|
|
207
|
+
},
|
|
208
|
+
codemodFile: {
|
|
209
|
+
type: "string",
|
|
210
|
+
alias: "c",
|
|
211
|
+
description: "Path to codemod file",
|
|
212
|
+
required: true
|
|
213
|
+
},
|
|
214
|
+
language: {
|
|
215
|
+
type: "string",
|
|
216
|
+
alias: "l",
|
|
217
|
+
description: "Language of the codemod",
|
|
218
|
+
required: true
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
async run({ args }) {
|
|
222
|
+
const { shardSize: shardSizeStr, stateProp, codeowners: codeownersPath, codemodFile: codemodFilePath, language } = args;
|
|
223
|
+
const shardSize = parseInt(shardSizeStr, 10);
|
|
224
|
+
if (isNaN(shardSize) || shardSize <= 0) {
|
|
225
|
+
console.error("Error: shard-size must be a positive number");
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
const stateOutputsPath = process.env.STATE_OUTPUTS;
|
|
229
|
+
if (!stateOutputsPath) {
|
|
230
|
+
console.error("Error: STATE_OUTPUTS environment variable is required");
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
console.log(`State property: ${stateProp}`);
|
|
235
|
+
const existingStateJson = process.env.CODEMOD_STATE;
|
|
236
|
+
let existingState;
|
|
237
|
+
if (existingStateJson) try {
|
|
238
|
+
existingState = JSON.parse(existingStateJson)[stateProp];
|
|
239
|
+
console.log(`Found existing state with ${existingState.length} shards`);
|
|
240
|
+
} catch (parseError) {
|
|
241
|
+
console.warn(`Warning: Failed to parse existing state: ${parseError}`);
|
|
242
|
+
existingState = void 0;
|
|
243
|
+
}
|
|
244
|
+
const result = await analyzeCodeowners({
|
|
245
|
+
shardSize,
|
|
246
|
+
codeownersPath,
|
|
247
|
+
rulePath: codemodFilePath,
|
|
248
|
+
projectRoot: process.cwd(),
|
|
249
|
+
language,
|
|
250
|
+
existingState
|
|
251
|
+
});
|
|
252
|
+
const stateOutput = `${stateProp}=${JSON.stringify(result.shards)}\n`;
|
|
253
|
+
console.log(`Writing state output to: ${stateOutputsPath}`);
|
|
254
|
+
await writeFile(stateOutputsPath, stateOutput, { flag: "a" });
|
|
255
|
+
console.log("✅ Sharding completed successfully!");
|
|
256
|
+
console.log("Generated shards:", JSON.stringify(result.shards, null, 2));
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error("❌ Failed to process codeowner file:");
|
|
259
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/commands/shard/directory.ts
|
|
267
|
+
/**
|
|
268
|
+
* Directory-based sharding command
|
|
269
|
+
*
|
|
270
|
+
* Creates shards by grouping files within subdirectories of a target directory.
|
|
271
|
+
* Uses consistent hashing to distribute files within each directory group, maintaining
|
|
272
|
+
* consistency with existing state when available.
|
|
273
|
+
*
|
|
274
|
+
* Example usage:
|
|
275
|
+
* npx codemodctl shard directory -l tsx -c ./codemod.ts -s 30 --stateProp shards --target packages/
|
|
276
|
+
*
|
|
277
|
+
* This will analyze all applicable files within subdirectories of 'packages/' and create
|
|
278
|
+
* shards with approximately 30 files each, grouped by directory.
|
|
279
|
+
*/
|
|
280
|
+
const directoryCommand = defineCommand({
|
|
281
|
+
meta: {
|
|
282
|
+
name: "directory",
|
|
283
|
+
description: "Create directory-based sharding output"
|
|
284
|
+
},
|
|
285
|
+
args: {
|
|
286
|
+
shardSize: {
|
|
287
|
+
type: "string",
|
|
288
|
+
alias: "s",
|
|
289
|
+
description: "Number of files per shard",
|
|
290
|
+
required: true
|
|
291
|
+
},
|
|
292
|
+
stateProp: {
|
|
293
|
+
type: "string",
|
|
294
|
+
alias: "p",
|
|
295
|
+
description: "Property name for state output",
|
|
296
|
+
required: true
|
|
297
|
+
},
|
|
298
|
+
target: {
|
|
299
|
+
type: "string",
|
|
300
|
+
description: "Target directory to shard by subdirectories",
|
|
301
|
+
required: true
|
|
302
|
+
},
|
|
303
|
+
codemodFile: {
|
|
304
|
+
type: "string",
|
|
305
|
+
alias: "c",
|
|
306
|
+
description: "Path to codemod file",
|
|
307
|
+
required: true
|
|
308
|
+
},
|
|
309
|
+
language: {
|
|
310
|
+
type: "string",
|
|
311
|
+
alias: "l",
|
|
312
|
+
description: "Language of the codemod",
|
|
313
|
+
required: true
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
async run({ args }) {
|
|
317
|
+
const { shardSize: shardSizeStr, stateProp, target, codemodFile: codemodFilePath, language } = args;
|
|
318
|
+
const shardSize = parseInt(shardSizeStr, 10);
|
|
319
|
+
if (isNaN(shardSize) || shardSize <= 0) {
|
|
320
|
+
console.error("Error: shard-size must be a positive number");
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
const stateOutputsPath = process.env.STATE_OUTPUTS;
|
|
324
|
+
if (!stateOutputsPath) {
|
|
325
|
+
console.error("Error: STATE_OUTPUTS environment variable is required");
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
console.log(`State property: ${stateProp}`);
|
|
330
|
+
console.log(`Target directory: ${target}`);
|
|
331
|
+
const existingStateJson = process.env.CODEMOD_STATE;
|
|
332
|
+
let existingState;
|
|
333
|
+
if (existingStateJson) try {
|
|
334
|
+
existingState = JSON.parse(existingStateJson)[stateProp];
|
|
335
|
+
console.log(`Found existing state with ${existingState.length} shards`);
|
|
336
|
+
} catch (parseError) {
|
|
337
|
+
console.warn(`Warning: Failed to parse existing state: ${parseError}`);
|
|
338
|
+
existingState = void 0;
|
|
339
|
+
}
|
|
340
|
+
const result = await analyzeDirectories({
|
|
341
|
+
shardSize,
|
|
342
|
+
target,
|
|
343
|
+
rulePath: codemodFilePath,
|
|
344
|
+
projectRoot: process.cwd(),
|
|
345
|
+
language,
|
|
346
|
+
existingState
|
|
347
|
+
});
|
|
348
|
+
const stateOutput = `${stateProp}=${JSON.stringify(result.shards)}\n`;
|
|
349
|
+
console.log(`Writing state output to: ${stateOutputsPath}`);
|
|
350
|
+
await writeFile(stateOutputsPath, stateOutput, { flag: "a" });
|
|
351
|
+
console.log("✅ Directory-based sharding completed successfully!");
|
|
352
|
+
console.log("Generated shards:", JSON.stringify(result.shards, null, 2));
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error("❌ Failed to process directory sharding:");
|
|
355
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/commands/shard/index.ts
|
|
363
|
+
const shardCommand = defineCommand({
|
|
364
|
+
meta: {
|
|
365
|
+
name: "shard",
|
|
366
|
+
description: "Sharding operations for distributing work"
|
|
367
|
+
},
|
|
368
|
+
subCommands: {
|
|
369
|
+
codeowner: codeownerCommand,
|
|
370
|
+
directory: directoryCommand
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/cli.ts
|
|
376
|
+
runMain(defineCommand({
|
|
377
|
+
meta: {
|
|
378
|
+
name: "codemodctl",
|
|
379
|
+
version: "0.1.0",
|
|
380
|
+
description: "CLI tool for workflow engine operations"
|
|
381
|
+
},
|
|
382
|
+
subCommands: {
|
|
383
|
+
git: gitCommand,
|
|
384
|
+
shard: shardCommand
|
|
385
|
+
}
|
|
386
|
+
}));
|
|
387
|
+
|
|
388
|
+
//#endregion
|
|
389
|
+
export { };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/codemod-cli.ts
|
|
5
|
+
/**
|
|
6
|
+
* Executes the codemod CLI command and returns applicable file paths
|
|
7
|
+
*/
|
|
8
|
+
async function getApplicableFiles(rulePath, language, projectRoot) {
|
|
9
|
+
try {
|
|
10
|
+
const command = `npx -y codemod@latest jssg list-applicable --no-interactive --allow-fs --allow-fetch --allow-child-process --language ${language} --target ${projectRoot} ${rulePath}`;
|
|
11
|
+
console.debug(`Executing: ${command}`);
|
|
12
|
+
const applicableFiles = execSync(command, {
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
cwd: projectRoot,
|
|
15
|
+
maxBuffer: 10 * 1024 * 1024
|
|
16
|
+
}).split("\n").filter((line) => line.startsWith("[Applicable] ")).map((line) => line.replace("[Applicable] ", "").trim()).filter((filePath) => filePath.length > 0);
|
|
17
|
+
console.debug(`Found ${applicableFiles.length} applicable files`);
|
|
18
|
+
return applicableFiles;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error("Error executing codemod CLI:", error);
|
|
21
|
+
throw new Error(`Failed to execute codemod CLI: ${error}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { getApplicableFiles as t };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//#region src/utils/codeowner-analysis.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Result for a single team-based shard
|
|
4
|
+
*/
|
|
5
|
+
interface ShardResult {
|
|
6
|
+
/** The team that owns these files */
|
|
7
|
+
team: string;
|
|
8
|
+
/** The shard identifier string (e.g., "1/3") */
|
|
9
|
+
shard: string;
|
|
10
|
+
/** The combined shard ID (e.g., "team-name 1/3") */
|
|
11
|
+
shardId: string;
|
|
12
|
+
/** Array of file paths in this shard */
|
|
13
|
+
_meta_files: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Information about a team and their files
|
|
17
|
+
*/
|
|
18
|
+
interface TeamFileInfo {
|
|
19
|
+
/** Team name */
|
|
20
|
+
team: string;
|
|
21
|
+
/** Number of files owned by this team */
|
|
22
|
+
fileCount: number;
|
|
23
|
+
/** Array of file paths owned by this team */
|
|
24
|
+
files: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Options for codeowner-based analysis
|
|
28
|
+
*/
|
|
29
|
+
interface CodeownerAnalysisOptions {
|
|
30
|
+
/** Target number of files per shard */
|
|
31
|
+
shardSize: number;
|
|
32
|
+
/** Optional path to CODEOWNERS file */
|
|
33
|
+
codeownersPath?: string;
|
|
34
|
+
/** Path to the codemod rule file */
|
|
35
|
+
rulePath: string;
|
|
36
|
+
/** Programming language for the codemod */
|
|
37
|
+
language: string;
|
|
38
|
+
/** Project root directory (defaults to process.cwd()) */
|
|
39
|
+
projectRoot?: string;
|
|
40
|
+
/** Existing state for consistency (optional) */
|
|
41
|
+
existingState?: ShardResult[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Result of codeowner-based analysis
|
|
45
|
+
*/
|
|
46
|
+
interface CodeownerAnalysisResult {
|
|
47
|
+
/** Array of team information */
|
|
48
|
+
teams: TeamFileInfo[];
|
|
49
|
+
/** Array of team-based shards with file assignments */
|
|
50
|
+
shards: ShardResult[];
|
|
51
|
+
/** Total number of files processed */
|
|
52
|
+
totalFiles: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Finds and resolves the CODEOWNERS file path
|
|
56
|
+
* Searches in common locations: root, .github/, docs/
|
|
57
|
+
* Returns null if no CODEOWNERS file is found
|
|
58
|
+
*/
|
|
59
|
+
declare function findCodeownersFile(projectRoot?: string, explicitPath?: string): Promise<string | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Normalizes owner name by removing `@` prefix and converting to lowercase
|
|
62
|
+
*/
|
|
63
|
+
declare function normalizeOwnerName(owner: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Analyzes files and groups them by codeowner team
|
|
66
|
+
*/
|
|
67
|
+
declare function analyzeFilesByOwner(codeownersPath: string, language: string, rulePath: string, projectRoot?: string): Promise<Map<string, string[]>>;
|
|
68
|
+
/**
|
|
69
|
+
* Analyzes files without codeowner parsing - assigns all files to "unassigned"
|
|
70
|
+
*/
|
|
71
|
+
declare function analyzeFilesWithoutOwner(language: string, rulePath: string, projectRoot?: string): Promise<Map<string, string[]>>;
|
|
72
|
+
/**
|
|
73
|
+
* Generates shard configuration from team file analysis with actual file distribution.
|
|
74
|
+
* Maintains consistency with existing state when provided.
|
|
75
|
+
*
|
|
76
|
+
* @param filesByOwner - Map of team names to their file arrays
|
|
77
|
+
* @param shardSize - Target number of files per shard
|
|
78
|
+
* @param existingState - Optional existing state for consistency
|
|
79
|
+
* @returns Array of team-based shards with file assignments
|
|
80
|
+
*/
|
|
81
|
+
declare function generateShards(filesByOwner: Map<string, string[]>, shardSize: number, existingState?: ShardResult[]): ShardResult[];
|
|
82
|
+
/**
|
|
83
|
+
* Converts file ownership map to team info array
|
|
84
|
+
*/
|
|
85
|
+
declare function getTeamFileInfo(filesByOwner: Map<string, string[]>): TeamFileInfo[];
|
|
86
|
+
/**
|
|
87
|
+
* Main function to analyze codeowners and generate shard configuration.
|
|
88
|
+
* Maintains consistency with existing state when provided.
|
|
89
|
+
*
|
|
90
|
+
* @param options - Configuration options for codeowner analysis
|
|
91
|
+
* @returns Promise resolving to codeowner analysis result
|
|
92
|
+
*/
|
|
93
|
+
declare function analyzeCodeowners(options: CodeownerAnalysisOptions): Promise<CodeownerAnalysisResult>;
|
|
94
|
+
//#endregion
|
|
95
|
+
export { analyzeCodeowners as a, findCodeownersFile as c, normalizeOwnerName as d, TeamFileInfo as i, generateShards as l, CodeownerAnalysisResult as n, analyzeFilesByOwner as o, ShardResult as r, analyzeFilesWithoutOwner as s, CodeownerAnalysisOptions as t, getTeamFileInfo as u };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as getApplicableFiles } from "./codemod-cli-Bg-LwFEk.js";
|
|
3
|
+
import Codeowners from "codeowners";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import path, { resolve } from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/utils/codeowner-analysis.ts
|
|
8
|
+
/**
|
|
9
|
+
* Finds and resolves the CODEOWNERS file path
|
|
10
|
+
* Searches in common locations: root, .github/, docs/
|
|
11
|
+
* Returns null if no CODEOWNERS file is found
|
|
12
|
+
*/
|
|
13
|
+
async function findCodeownersFile(projectRoot = process.cwd(), explicitPath) {
|
|
14
|
+
if (explicitPath) {
|
|
15
|
+
const resolvedPath = resolve(explicitPath);
|
|
16
|
+
if (!existsSync(resolvedPath)) throw new Error(`CODEOWNERS file not found at: ${resolvedPath}`);
|
|
17
|
+
return resolvedPath;
|
|
18
|
+
}
|
|
19
|
+
const searchPaths = [
|
|
20
|
+
resolve(projectRoot, "CODEOWNERS"),
|
|
21
|
+
resolve(projectRoot, ".github", "CODEOWNERS"),
|
|
22
|
+
resolve(projectRoot, "docs", "CODEOWNERS")
|
|
23
|
+
];
|
|
24
|
+
for (const searchPath of searchPaths) if (existsSync(searchPath)) return searchPath;
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Normalizes owner name by removing `@` prefix and converting to lowercase
|
|
29
|
+
*/
|
|
30
|
+
function normalizeOwnerName(owner) {
|
|
31
|
+
return owner.replace("@", "").toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Analyzes files and groups them by codeowner team
|
|
35
|
+
*/
|
|
36
|
+
async function analyzeFilesByOwner(codeownersPath, language, rulePath, projectRoot = process.cwd()) {
|
|
37
|
+
const codeowners = new Codeowners(codeownersPath);
|
|
38
|
+
const gitRootDir = codeowners.codeownersDirectory;
|
|
39
|
+
const filesByOwner = /* @__PURE__ */ new Map();
|
|
40
|
+
const applicableFiles = await getApplicableFiles(rulePath, language, projectRoot);
|
|
41
|
+
for (const filePath of applicableFiles) {
|
|
42
|
+
const absolutePath = path.resolve(projectRoot, filePath);
|
|
43
|
+
const relativePath = path.relative(gitRootDir, absolutePath);
|
|
44
|
+
const owners = codeowners.getOwner(relativePath);
|
|
45
|
+
let ownerKey;
|
|
46
|
+
if (owners && owners.length > 0) {
|
|
47
|
+
const owner = owners[0];
|
|
48
|
+
ownerKey = normalizeOwnerName(owner ?? "unknown");
|
|
49
|
+
} else ownerKey = "unassigned";
|
|
50
|
+
if (!filesByOwner.has(ownerKey)) filesByOwner.set(ownerKey, []);
|
|
51
|
+
filesByOwner.get(ownerKey)?.push(relativePath);
|
|
52
|
+
}
|
|
53
|
+
return filesByOwner;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Analyzes files without codeowner parsing - assigns all files to "unassigned"
|
|
57
|
+
*/
|
|
58
|
+
async function analyzeFilesWithoutOwner(language, rulePath, projectRoot = process.cwd()) {
|
|
59
|
+
const filesByOwner = /* @__PURE__ */ new Map();
|
|
60
|
+
const unassignedFiles = (await getApplicableFiles(rulePath, language, projectRoot)).map((filePath) => {
|
|
61
|
+
return path.relative(projectRoot, path.resolve(projectRoot, filePath));
|
|
62
|
+
});
|
|
63
|
+
filesByOwner.set("unassigned", unassignedFiles);
|
|
64
|
+
return filesByOwner;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Calculate optimal number of shards based on target shard size
|
|
68
|
+
*
|
|
69
|
+
* @param totalFiles - Total number of files
|
|
70
|
+
* @param targetShardSize - Desired number of files per shard
|
|
71
|
+
* @returns Number of shards needed
|
|
72
|
+
*/
|
|
73
|
+
function calculateOptimalShardCount(totalFiles, targetShardSize) {
|
|
74
|
+
return Math.ceil(totalFiles / targetShardSize);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Generates shard configuration from team file analysis with actual file distribution.
|
|
78
|
+
* Maintains consistency with existing state when provided.
|
|
79
|
+
*
|
|
80
|
+
* @param filesByOwner - Map of team names to their file arrays
|
|
81
|
+
* @param shardSize - Target number of files per shard
|
|
82
|
+
* @param existingState - Optional existing state for consistency
|
|
83
|
+
* @returns Array of team-based shards with file assignments
|
|
84
|
+
*/
|
|
85
|
+
function generateShards(filesByOwner, shardSize, existingState) {
|
|
86
|
+
const allShards = [];
|
|
87
|
+
const existingByTeam = /* @__PURE__ */ new Map();
|
|
88
|
+
if (existingState) for (const shard of existingState) {
|
|
89
|
+
if (!existingByTeam.has(shard.team)) existingByTeam.set(shard.team, []);
|
|
90
|
+
existingByTeam.get(shard.team)?.push(shard);
|
|
91
|
+
}
|
|
92
|
+
for (const [team, files] of filesByOwner.entries()) {
|
|
93
|
+
const fileCount = files.length;
|
|
94
|
+
const optimalShardCount = calculateOptimalShardCount(fileCount, shardSize);
|
|
95
|
+
const existingShardCount = (existingByTeam.get(team) || []).length;
|
|
96
|
+
const numShards = existingShardCount > 0 ? existingShardCount : optimalShardCount;
|
|
97
|
+
console.log(`Team "${team}" owns ${fileCount} files, ${existingShardCount > 0 ? `maintaining ${numShards} existing shards` : `creating ${numShards} new shards`}`);
|
|
98
|
+
const sortedFiles = [...files].sort();
|
|
99
|
+
for (let i = 1; i <= numShards; i++) {
|
|
100
|
+
const shardFiles = [];
|
|
101
|
+
for (let fileIndex = i - 1; fileIndex < sortedFiles.length; fileIndex += numShards) shardFiles.push(sortedFiles[fileIndex] ?? "");
|
|
102
|
+
allShards.push({
|
|
103
|
+
team,
|
|
104
|
+
shard: `${i}/${numShards}`,
|
|
105
|
+
shardId: `${team} ${i}/${numShards}`,
|
|
106
|
+
_meta_files: shardFiles
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return allShards;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Converts file ownership map to team info array
|
|
114
|
+
*/
|
|
115
|
+
function getTeamFileInfo(filesByOwner) {
|
|
116
|
+
return Array.from(filesByOwner.entries()).map(([team, files]) => ({
|
|
117
|
+
team,
|
|
118
|
+
fileCount: files.length,
|
|
119
|
+
files
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Main function to analyze codeowners and generate shard configuration.
|
|
124
|
+
* Maintains consistency with existing state when provided.
|
|
125
|
+
*
|
|
126
|
+
* @param options - Configuration options for codeowner analysis
|
|
127
|
+
* @returns Promise resolving to codeowner analysis result
|
|
128
|
+
*/
|
|
129
|
+
async function analyzeCodeowners(options) {
|
|
130
|
+
const { shardSize, codeownersPath, rulePath, language, projectRoot = process.cwd(), existingState } = options;
|
|
131
|
+
const resolvedCodeownersPath = await findCodeownersFile(projectRoot, codeownersPath);
|
|
132
|
+
let filesByOwner;
|
|
133
|
+
console.debug(`Using rule file: ${rulePath}`);
|
|
134
|
+
console.debug(`Shard size: ${shardSize}`);
|
|
135
|
+
if (resolvedCodeownersPath) {
|
|
136
|
+
console.log(`Analyzing CODEOWNERS file: ${resolvedCodeownersPath}`);
|
|
137
|
+
console.log("Analyzing files with CLI command...");
|
|
138
|
+
filesByOwner = await analyzeFilesByOwner(resolvedCodeownersPath, language, rulePath, projectRoot);
|
|
139
|
+
} else {
|
|
140
|
+
console.log("No CODEOWNERS file found, assigning all files to 'unassigned'");
|
|
141
|
+
console.log("Analyzing files with CLI command...");
|
|
142
|
+
filesByOwner = await analyzeFilesWithoutOwner(language, rulePath, projectRoot);
|
|
143
|
+
}
|
|
144
|
+
console.log("File analysis completed. Generating shards...");
|
|
145
|
+
if (existingState) console.debug(`Using existing state with ${existingState.length} shards`);
|
|
146
|
+
const teams = getTeamFileInfo(filesByOwner);
|
|
147
|
+
const shards = generateShards(filesByOwner, shardSize, existingState);
|
|
148
|
+
const totalFiles = Array.from(filesByOwner.values()).reduce((sum, files) => sum + files.length, 0);
|
|
149
|
+
console.log(`Generated ${shards.length} total shards for ${totalFiles} files`);
|
|
150
|
+
return {
|
|
151
|
+
teams,
|
|
152
|
+
shards,
|
|
153
|
+
totalFiles
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
export { generateShards as a, findCodeownersFile as i, analyzeFilesByOwner as n, getTeamFileInfo as o, analyzeFilesWithoutOwner as r, normalizeOwnerName as s, analyzeCodeowners as t };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as analyzeCodeowners, c as findCodeownersFile, d as normalizeOwnerName, i as TeamFileInfo, l as generateShards, n as CodeownerAnalysisResult, o as analyzeFilesByOwner, r as ShardResult, s as analyzeFilesWithoutOwner, t as CodeownerAnalysisOptions, u as getTeamFileInfo } from "./codeowner-analysis-BAoreMb0.js";
|
|
2
|
+
export { CodeownerAnalysisOptions, CodeownerAnalysisResult, ShardResult, TeamFileInfo, analyzeCodeowners, analyzeFilesByOwner, analyzeFilesWithoutOwner, findCodeownersFile, generateShards, getTeamFileInfo, normalizeOwnerName };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./codemod-cli-Bg-LwFEk.js";
|
|
3
|
+
import { a as generateShards, i as findCodeownersFile, n as analyzeFilesByOwner, o as getTeamFileInfo, r as analyzeFilesWithoutOwner, s as normalizeOwnerName, t as analyzeCodeowners } from "./codeowner-analysis-pEygA0IV.js";
|
|
4
|
+
|
|
5
|
+
export { analyzeCodeowners, analyzeFilesByOwner, analyzeFilesWithoutOwner, findCodeownersFile, generateShards, getTeamFileInfo, normalizeOwnerName };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/consistent-sharding.ts
|
|
5
|
+
const HASH_RING_SIZE = 1e6;
|
|
6
|
+
/**
|
|
7
|
+
* Generates a numeric hash from a filename using SHA1
|
|
8
|
+
* Uses only the first 8 characters of the hex digest to avoid JavaScript number precision issues
|
|
9
|
+
*/
|
|
10
|
+
function getNumericFileNameSha1(filename) {
|
|
11
|
+
const hex = crypto.createHash("sha1").update(filename).digest("hex").substring(0, 8);
|
|
12
|
+
return parseInt(hex, 16);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Maps a filename to a consistent position on the hash ring (0 to HASH_RING_SIZE-1)
|
|
16
|
+
* This position remains constant regardless of shard count changes
|
|
17
|
+
*/
|
|
18
|
+
function getFileHashPosition(filename) {
|
|
19
|
+
return getNumericFileNameSha1(filename) % HASH_RING_SIZE;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the position for a specific shard index on the hash ring
|
|
23
|
+
* Shards get fixed positions that don't change when other shards are added
|
|
24
|
+
*/
|
|
25
|
+
function getShardPosition(shardIndex) {
|
|
26
|
+
return parseInt(crypto.createHash("sha1").update(`shard-${shardIndex}`).digest("hex").substring(0, 8), 16) % HASH_RING_SIZE;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Gets the shard index for a filename using consistent hashing
|
|
30
|
+
* Files are assigned to the next shard clockwise on the hash ring
|
|
31
|
+
*
|
|
32
|
+
* @param filename - The file path to hash
|
|
33
|
+
* @param shardCount - Total number of shards
|
|
34
|
+
* @returns Shard index (0-based)
|
|
35
|
+
*/
|
|
36
|
+
function getShardForFilename(filename, { shardCount }) {
|
|
37
|
+
if (shardCount <= 0) throw new Error("Shard count must be greater than 0");
|
|
38
|
+
const filePosition = getFileHashPosition(filename);
|
|
39
|
+
const shardInfo = [];
|
|
40
|
+
for (let i = 0; i < shardCount; i++) shardInfo.push({
|
|
41
|
+
index: i,
|
|
42
|
+
position: getShardPosition(i)
|
|
43
|
+
});
|
|
44
|
+
shardInfo.sort((a, b) => a.position - b.position);
|
|
45
|
+
for (const shard of shardInfo) if (filePosition <= shard.position) return shard.index;
|
|
46
|
+
return shardInfo[0]?.index ?? 0;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Checks if a file belongs to a specific shard by simply checking if it's in the shard's files list
|
|
50
|
+
*
|
|
51
|
+
* @param filename - The file path to check
|
|
52
|
+
* @param shard - Shard object containing files array
|
|
53
|
+
* @returns True if file is in the shard's files list
|
|
54
|
+
*/
|
|
55
|
+
function fitsInShard(filename, shard) {
|
|
56
|
+
return shard._meta_files.includes(filename);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Distributes files across shards using deterministic hashing
|
|
60
|
+
*
|
|
61
|
+
* @param filenames - Array of file paths
|
|
62
|
+
* @param shardCount - Total number of shards
|
|
63
|
+
* @returns Map of shard index to array of filenames
|
|
64
|
+
*/
|
|
65
|
+
function distributeFilesAcrossShards(filenames, shardCount) {
|
|
66
|
+
if (shardCount <= 0) throw new Error("Shard count must be greater than 0");
|
|
67
|
+
const shardMap = /* @__PURE__ */ new Map();
|
|
68
|
+
for (let i = 0; i < shardCount; i++) shardMap.set(i, []);
|
|
69
|
+
for (const filename of filenames) {
|
|
70
|
+
const shardIndex = getShardForFilename(filename, { shardCount });
|
|
71
|
+
shardMap.get(shardIndex)?.push(filename);
|
|
72
|
+
}
|
|
73
|
+
return shardMap;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Calculate optimal number of shards based on target shard size
|
|
77
|
+
*
|
|
78
|
+
* @param totalFiles - Total number of files
|
|
79
|
+
* @param targetShardSize - Desired number of files per shard
|
|
80
|
+
* @returns Number of shards needed
|
|
81
|
+
*/
|
|
82
|
+
function calculateOptimalShardCount(totalFiles, targetShardSize) {
|
|
83
|
+
return Math.ceil(totalFiles / targetShardSize);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Analyzes file reassignment when scaling from oldShardCount to newShardCount
|
|
87
|
+
* Returns statistics about how many files would need to be reassigned
|
|
88
|
+
*
|
|
89
|
+
* @param filenames - Array of file paths to analyze
|
|
90
|
+
* @param oldShardCount - Current number of shards
|
|
91
|
+
* @param newShardCount - Target number of shards
|
|
92
|
+
* @returns Object with reassignment statistics
|
|
93
|
+
*/
|
|
94
|
+
function analyzeShardScaling(filenames, oldShardCount, newShardCount) {
|
|
95
|
+
let reassignedFiles = 0;
|
|
96
|
+
for (const filename of filenames) if (getShardForFilename(filename, { shardCount: oldShardCount }) !== getShardForFilename(filename, { shardCount: newShardCount })) reassignedFiles++;
|
|
97
|
+
const stableFiles = filenames.length - reassignedFiles;
|
|
98
|
+
const reassignmentPercentage = filenames.length > 0 ? reassignedFiles / filenames.length * 100 : 0;
|
|
99
|
+
return {
|
|
100
|
+
totalFiles: filenames.length,
|
|
101
|
+
reassignedFiles,
|
|
102
|
+
reassignmentPercentage,
|
|
103
|
+
stableFiles
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
export { getFileHashPosition as a, fitsInShard as i, calculateOptimalShardCount as n, getNumericFileNameSha1 as o, distributeFilesAcrossShards as r, getShardForFilename as s, analyzeShardScaling as t };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//#region src/utils/consistent-sharding.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Generates a numeric hash from a filename using SHA1
|
|
4
|
+
* Uses only the first 8 characters of the hex digest to avoid JavaScript number precision issues
|
|
5
|
+
*/
|
|
6
|
+
declare function getNumericFileNameSha1(filename: string): number;
|
|
7
|
+
/**
|
|
8
|
+
* Maps a filename to a consistent position on the hash ring (0 to HASH_RING_SIZE-1)
|
|
9
|
+
* This position remains constant regardless of shard count changes
|
|
10
|
+
*/
|
|
11
|
+
declare function getFileHashPosition(filename: string): number;
|
|
12
|
+
/**
|
|
13
|
+
* Gets the shard index for a filename using consistent hashing
|
|
14
|
+
* Files are assigned to the next shard clockwise on the hash ring
|
|
15
|
+
*
|
|
16
|
+
* @param filename - The file path to hash
|
|
17
|
+
* @param shardCount - Total number of shards
|
|
18
|
+
* @returns Shard index (0-based)
|
|
19
|
+
*/
|
|
20
|
+
declare function getShardForFilename(filename: string, {
|
|
21
|
+
shardCount
|
|
22
|
+
}: {
|
|
23
|
+
shardCount: number;
|
|
24
|
+
}): number;
|
|
25
|
+
/**
|
|
26
|
+
* Checks if a file belongs to a specific shard by simply checking if it's in the shard's files list
|
|
27
|
+
*
|
|
28
|
+
* @param filename - The file path to check
|
|
29
|
+
* @param shard - Shard object containing files array
|
|
30
|
+
* @returns True if file is in the shard's files list
|
|
31
|
+
*/
|
|
32
|
+
declare function fitsInShard(filename: string, shard: {
|
|
33
|
+
_meta_files: string[];
|
|
34
|
+
}): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Distributes files across shards using deterministic hashing
|
|
37
|
+
*
|
|
38
|
+
* @param filenames - Array of file paths
|
|
39
|
+
* @param shardCount - Total number of shards
|
|
40
|
+
* @returns Map of shard index to array of filenames
|
|
41
|
+
*/
|
|
42
|
+
declare function distributeFilesAcrossShards(filenames: string[], shardCount: number): Map<number, string[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Calculate optimal number of shards based on target shard size
|
|
45
|
+
*
|
|
46
|
+
* @param totalFiles - Total number of files
|
|
47
|
+
* @param targetShardSize - Desired number of files per shard
|
|
48
|
+
* @returns Number of shards needed
|
|
49
|
+
*/
|
|
50
|
+
declare function calculateOptimalShardCount(totalFiles: number, targetShardSize: number): number;
|
|
51
|
+
/**
|
|
52
|
+
* Analyzes file reassignment when scaling from oldShardCount to newShardCount
|
|
53
|
+
* Returns statistics about how many files would need to be reassigned
|
|
54
|
+
*
|
|
55
|
+
* @param filenames - Array of file paths to analyze
|
|
56
|
+
* @param oldShardCount - Current number of shards
|
|
57
|
+
* @param newShardCount - Target number of shards
|
|
58
|
+
* @returns Object with reassignment statistics
|
|
59
|
+
*/
|
|
60
|
+
declare function analyzeShardScaling(filenames: string[], oldShardCount: number, newShardCount: number): {
|
|
61
|
+
totalFiles: number;
|
|
62
|
+
reassignedFiles: number;
|
|
63
|
+
reassignmentPercentage: number;
|
|
64
|
+
stableFiles: number;
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
export { getFileHashPosition as a, fitsInShard as i, calculateOptimalShardCount as n, getNumericFileNameSha1 as o, distributeFilesAcrossShards as r, getShardForFilename as s, analyzeShardScaling as t };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as getApplicableFiles } from "./codemod-cli-Bg-LwFEk.js";
|
|
3
|
+
import { n as calculateOptimalShardCount, r as distributeFilesAcrossShards } from "./consistent-sharding-CQg-qiBA.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/utils/directory-analysis.ts
|
|
7
|
+
/**
|
|
8
|
+
* Groups files by their immediate subdirectory within the target directory
|
|
9
|
+
*
|
|
10
|
+
* @param files - Array of file paths to group
|
|
11
|
+
* @param target - Target directory to analyze subdirectories within
|
|
12
|
+
* @param projectRoot - Root directory of the project for resolving relative paths
|
|
13
|
+
* @returns Map of subdirectory paths to their file lists
|
|
14
|
+
*/
|
|
15
|
+
function groupFilesByDirectory(files, target, projectRoot) {
|
|
16
|
+
const resolvedTarget = path.resolve(projectRoot, target);
|
|
17
|
+
const filesByDirectory = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const filePath of files) {
|
|
19
|
+
const normalizedFile = path.normalize(filePath);
|
|
20
|
+
const resolvedFile = path.resolve(projectRoot, normalizedFile);
|
|
21
|
+
if (!resolvedFile.startsWith(resolvedTarget)) continue;
|
|
22
|
+
const relativePath = path.relative(projectRoot, resolvedFile);
|
|
23
|
+
const relativeFromTarget = path.relative(resolvedTarget, resolvedFile);
|
|
24
|
+
if (!relativeFromTarget.includes(path.sep)) continue;
|
|
25
|
+
const firstDir = relativeFromTarget.split(path.sep)[0];
|
|
26
|
+
if (!firstDir) continue;
|
|
27
|
+
const subdirectory = path.relative(projectRoot, path.join(resolvedTarget, firstDir));
|
|
28
|
+
if (!filesByDirectory.has(subdirectory)) filesByDirectory.set(subdirectory, []);
|
|
29
|
+
filesByDirectory.get(subdirectory)?.push(relativePath);
|
|
30
|
+
}
|
|
31
|
+
return filesByDirectory;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Creates directory-based shards using consistent hashing within each directory group.
|
|
35
|
+
* Maintains consistency with existing state when provided.
|
|
36
|
+
*
|
|
37
|
+
* @param filesByDirectory - Map of directory paths to their file lists
|
|
38
|
+
* @param shardSize - Target number of files per shard
|
|
39
|
+
* @param existingState - Optional existing state for consistency
|
|
40
|
+
* @returns Array of directory-based shards
|
|
41
|
+
*/
|
|
42
|
+
function createDirectoryShards(filesByDirectory, shardSize, existingState) {
|
|
43
|
+
const allShards = [];
|
|
44
|
+
const existingByDirectory = /* @__PURE__ */ new Map();
|
|
45
|
+
if (existingState) for (const shard of existingState) {
|
|
46
|
+
if (!existingByDirectory.has(shard.directory)) existingByDirectory.set(shard.directory, []);
|
|
47
|
+
existingByDirectory.get(shard.directory)?.push(shard);
|
|
48
|
+
}
|
|
49
|
+
for (const [directory, files] of filesByDirectory.entries()) {
|
|
50
|
+
const fileCount = files.length;
|
|
51
|
+
const optimalShardCount = calculateOptimalShardCount(fileCount, shardSize);
|
|
52
|
+
const existingShards = existingByDirectory.get(directory) || [];
|
|
53
|
+
const existingShardCount = existingShards.length > 0 ? existingShards[0]?.shardCount ?? 0 : 0;
|
|
54
|
+
const shardCount = existingShardCount > 0 ? existingShardCount : optimalShardCount;
|
|
55
|
+
console.log(`Directory "${directory}" contains ${fileCount} files, ${existingShardCount > 0 ? `maintaining ${shardCount} existing shards` : `creating ${shardCount} new shards`}`);
|
|
56
|
+
const shardMap = distributeFilesAcrossShards(files, shardCount);
|
|
57
|
+
for (let shardIndex = 0; shardIndex < shardCount; shardIndex++) {
|
|
58
|
+
const shardFiles = shardMap.get(shardIndex) || [];
|
|
59
|
+
allShards.push({
|
|
60
|
+
directory,
|
|
61
|
+
shard: shardIndex + 1,
|
|
62
|
+
shardCount,
|
|
63
|
+
_meta_files: shardFiles.sort(),
|
|
64
|
+
name: `${directory} (${shardIndex + 1}/${shardCount})`
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return allShards;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Main function to analyze directories and generate shard configuration.
|
|
72
|
+
* Maintains consistency with existing state when provided.
|
|
73
|
+
*
|
|
74
|
+
* @param options - Configuration options for directory analysis
|
|
75
|
+
* @returns Promise resolving to directory analysis result
|
|
76
|
+
* @throws Error if no files found in target subdirectories
|
|
77
|
+
*/
|
|
78
|
+
async function analyzeDirectories(options) {
|
|
79
|
+
const { shardSize, target, rulePath, language, projectRoot = process.cwd(), existingState } = options;
|
|
80
|
+
if (existingState) console.debug(`Using existing state with ${existingState.length} shards`);
|
|
81
|
+
console.log("Analyzing files with CLI command...");
|
|
82
|
+
const applicableFiles = await getApplicableFiles(rulePath, language, projectRoot);
|
|
83
|
+
console.log("Grouping files by directory...");
|
|
84
|
+
const filesByDirectory = groupFilesByDirectory(applicableFiles, path.resolve(projectRoot, target), projectRoot);
|
|
85
|
+
if (filesByDirectory.size === 0) throw new Error(`No files found in subdirectories of target: ${target}`);
|
|
86
|
+
console.log(`Found ${filesByDirectory.size} subdirectories in target`);
|
|
87
|
+
console.log("Generating directory-based shards...");
|
|
88
|
+
const shards = createDirectoryShards(filesByDirectory, shardSize, existingState);
|
|
89
|
+
const totalFiles = Array.from(filesByDirectory.values()).reduce((sum, files) => sum + files.length, 0);
|
|
90
|
+
console.log(`Generated ${shards.length} total shards for ${totalFiles} files across ${filesByDirectory.size} directories`);
|
|
91
|
+
return {
|
|
92
|
+
shards,
|
|
93
|
+
totalFiles
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
export { createDirectoryShards as n, groupFilesByDirectory as r, analyzeDirectories as t };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region src/utils/directory-analysis.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Result for a single directory-based shard
|
|
4
|
+
*/
|
|
5
|
+
interface DirectoryShardResult {
|
|
6
|
+
/** The directory path this shard belongs to */
|
|
7
|
+
directory: string;
|
|
8
|
+
/** The shard number (1-based) within this directory */
|
|
9
|
+
shard: number;
|
|
10
|
+
/** Total number of shards for this directory */
|
|
11
|
+
shardCount: number;
|
|
12
|
+
/** Array of file paths in this shard */
|
|
13
|
+
_meta_files: string[];
|
|
14
|
+
/** The name of the shard */
|
|
15
|
+
name: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Options for directory-based analysis
|
|
19
|
+
*/
|
|
20
|
+
interface DirectoryAnalysisOptions {
|
|
21
|
+
/** Target number of files per shard */
|
|
22
|
+
shardSize: number;
|
|
23
|
+
/** Target directory to analyze subdirectories within */
|
|
24
|
+
target: string;
|
|
25
|
+
/** Path to the codemod rule file */
|
|
26
|
+
rulePath: string;
|
|
27
|
+
/** Programming language for the codemod */
|
|
28
|
+
language: string;
|
|
29
|
+
/** Project root directory (defaults to process.cwd()) */
|
|
30
|
+
projectRoot?: string;
|
|
31
|
+
/** Existing state for consistency (optional) */
|
|
32
|
+
existingState?: DirectoryShardResult[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Result of directory-based analysis
|
|
36
|
+
*/
|
|
37
|
+
interface DirectoryAnalysisResult {
|
|
38
|
+
/** Array of directory-based shards */
|
|
39
|
+
shards: DirectoryShardResult[];
|
|
40
|
+
/** Total number of files processed */
|
|
41
|
+
totalFiles: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Groups files by their immediate subdirectory within the target directory
|
|
45
|
+
*
|
|
46
|
+
* @param files - Array of file paths to group
|
|
47
|
+
* @param target - Target directory to analyze subdirectories within
|
|
48
|
+
* @param projectRoot - Root directory of the project for resolving relative paths
|
|
49
|
+
* @returns Map of subdirectory paths to their file lists
|
|
50
|
+
*/
|
|
51
|
+
declare function groupFilesByDirectory(files: string[], target: string, projectRoot: string): Map<string, string[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Creates directory-based shards using consistent hashing within each directory group.
|
|
54
|
+
* Maintains consistency with existing state when provided.
|
|
55
|
+
*
|
|
56
|
+
* @param filesByDirectory - Map of directory paths to their file lists
|
|
57
|
+
* @param shardSize - Target number of files per shard
|
|
58
|
+
* @param existingState - Optional existing state for consistency
|
|
59
|
+
* @returns Array of directory-based shards
|
|
60
|
+
*/
|
|
61
|
+
declare function createDirectoryShards(filesByDirectory: Map<string, string[]>, shardSize: number, existingState?: DirectoryShardResult[]): DirectoryShardResult[];
|
|
62
|
+
/**
|
|
63
|
+
* Main function to analyze directories and generate shard configuration.
|
|
64
|
+
* Maintains consistency with existing state when provided.
|
|
65
|
+
*
|
|
66
|
+
* @param options - Configuration options for directory analysis
|
|
67
|
+
* @returns Promise resolving to directory analysis result
|
|
68
|
+
* @throws Error if no files found in target subdirectories
|
|
69
|
+
*/
|
|
70
|
+
declare function analyzeDirectories(options: DirectoryAnalysisOptions): Promise<DirectoryAnalysisResult>;
|
|
71
|
+
//#endregion
|
|
72
|
+
export { DirectoryAnalysisOptions, DirectoryAnalysisResult, DirectoryShardResult, analyzeDirectories, createDirectoryShards, groupFilesByDirectory };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./codemod-cli-Bg-LwFEk.js";
|
|
3
|
+
import "./consistent-sharding-CQg-qiBA.js";
|
|
4
|
+
import { n as createDirectoryShards, r as groupFilesByDirectory, t as analyzeDirectories } from "./directory-analysis-C4q0A0YM.js";
|
|
5
|
+
|
|
6
|
+
export { analyzeDirectories, createDirectoryShards, groupFilesByDirectory };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as getFileHashPosition, i as fitsInShard, n as calculateOptimalShardCount, o as getNumericFileNameSha1, r as distributeFilesAcrossShards, s as getShardForFilename, t as analyzeShardScaling } from "./consistent-sharding-bHfBogeY.js";
|
|
2
|
+
import { a as analyzeCodeowners, c as findCodeownersFile, d as normalizeOwnerName, i as TeamFileInfo, l as generateShards, n as CodeownerAnalysisResult, o as analyzeFilesByOwner, r as ShardResult, s as analyzeFilesWithoutOwner, t as CodeownerAnalysisOptions, u as getTeamFileInfo } from "./codeowner-analysis-BAoreMb0.js";
|
|
3
|
+
export { CodeownerAnalysisOptions, CodeownerAnalysisResult, ShardResult, TeamFileInfo, analyzeCodeowners, analyzeFilesByOwner, analyzeFilesWithoutOwner, analyzeShardScaling, calculateOptimalShardCount, distributeFilesAcrossShards, findCodeownersFile, fitsInShard, generateShards, getFileHashPosition, getNumericFileNameSha1, getShardForFilename, getTeamFileInfo, normalizeOwnerName };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./codemod-cli-Bg-LwFEk.js";
|
|
3
|
+
import { a as generateShards, i as findCodeownersFile, n as analyzeFilesByOwner, o as getTeamFileInfo, r as analyzeFilesWithoutOwner, s as normalizeOwnerName, t as analyzeCodeowners } from "./codeowner-analysis-pEygA0IV.js";
|
|
4
|
+
import { a as getFileHashPosition, i as fitsInShard, n as calculateOptimalShardCount, o as getNumericFileNameSha1, r as distributeFilesAcrossShards, s as getShardForFilename, t as analyzeShardScaling } from "./consistent-sharding-CQg-qiBA.js";
|
|
5
|
+
|
|
6
|
+
export { analyzeCodeowners, analyzeFilesByOwner, analyzeFilesWithoutOwner, analyzeShardScaling, calculateOptimalShardCount, distributeFilesAcrossShards, findCodeownersFile, fitsInShard, generateShards, getFileHashPosition, getNumericFileNameSha1, getShardForFilename, getTeamFileInfo, normalizeOwnerName };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as getFileHashPosition, i as fitsInShard, n as calculateOptimalShardCount, o as getNumericFileNameSha1, r as distributeFilesAcrossShards, s as getShardForFilename, t as analyzeShardScaling } from "./consistent-sharding-bHfBogeY.js";
|
|
2
|
+
export { analyzeShardScaling, calculateOptimalShardCount, distributeFilesAcrossShards, fitsInShard, getFileHashPosition, getNumericFileNameSha1, getShardForFilename };
|
package/dist/sharding.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as getFileHashPosition, i as fitsInShard, n as calculateOptimalShardCount, o as getNumericFileNameSha1, r as distributeFilesAcrossShards, s as getShardForFilename, t as analyzeShardScaling } from "./consistent-sharding-CQg-qiBA.js";
|
|
3
|
+
|
|
4
|
+
export { analyzeShardScaling, calculateOptimalShardCount, distributeFilesAcrossShards, fitsInShard, getFileHashPosition, getNumericFileNameSha1, getShardForFilename };
|