@kamaalio/codemod-kit 0.0.30 → 0.0.32

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.
@@ -1,2 +1,2 @@
1
- export { runCodemods, runCodemod, commitEditModifications, findAndReplace, findAndReplaceEdits, findAndReplaceConfig, findAndReplaceConfigModifications, traverseUp, } from './utils.js';
2
- export type { Codemod, Modifications, FindAndReplaceConfig, RunCodemodOkResult, RunCodemodResult } from './types.js';
1
+ export { runCodemods, runCodemod, commitEditModifications, findAndReplace, findAndReplaceEdits, findAndReplaceConfig, findAndReplaceConfigModifications, traverseUp, runCodemodsOnProjects, } from './utils.js';
2
+ export type { Codemod, Modifications, FindAndReplaceConfig, RunCodemodOkResult, RunCodemodResult, CodemodRunnerCodemod, RepositoryToClone, } from './types.js';
@@ -19,6 +19,14 @@ export type Codemod = {
19
19
  results: Array<RunCodemodOkResult>;
20
20
  }) => Promise<void>;
21
21
  };
22
+ export type CodemodRunnerCodemod<Tag = string, C extends Codemod = Codemod> = C & {
23
+ tags: Set<Tag> | Array<Tag>;
24
+ commitMessage: string;
25
+ };
26
+ export type RepositoryToClone<Tag = string> = {
27
+ address: string;
28
+ tags: Set<Tag>;
29
+ };
22
30
  export type ModificationsReport = {
23
31
  changesApplied: number;
24
32
  };
@@ -2,20 +2,24 @@ import { type Edit, type SgRoot, type SgNode } from '@ast-grep/napi';
2
2
  import type { Kinds, TypesMap } from '@ast-grep/napi/types/staticTypes.js';
3
3
  import type { NapiLang } from '@ast-grep/napi/types/lang.js';
4
4
  import { type types } from '@kamaalio/kamaal';
5
- import type { Codemod, FindAndReplaceConfig, Modifications, RunCodemodResult } from './types.js';
6
- type RunCodemodHooks<C extends Codemod> = {
5
+ import type { Codemod, CodemodRunnerCodemod, FindAndReplaceConfig, Modifications, RepositoryToClone, RunCodemodResult } from './types.js';
6
+ type RunCodemodHooks<C extends Codemod = Codemod> = {
7
7
  targetFiltering?: (filepath: string, codemod: C) => boolean;
8
8
  preCodemodRun?: (codemod: C) => Promise<void>;
9
9
  postTransform?: (transformedContent: string, codemod: C) => Promise<string>;
10
10
  };
11
- type RunCodemodOptions<C extends Codemod> = {
11
+ type RunCodemodOptions<C extends Codemod = Codemod> = {
12
12
  hooks?: RunCodemodHooks<C>;
13
13
  log?: boolean;
14
14
  dry?: boolean;
15
15
  rootPaths?: Array<string>;
16
16
  };
17
- export declare function runCodemods<C extends Codemod>(codemods: Array<C>, transformationPath: string, options?: RunCodemodOptions<C>): Promise<Record<string, Array<RunCodemodResult>>>;
18
- export declare function runCodemod<C extends Codemod>(codemod: C, transformationPath: string, options?: RunCodemodOptions<C>): Promise<Array<RunCodemodResult>>;
17
+ export declare function runCodemodsOnProjects<Tag = string, C extends Codemod = Codemod>(repositoriesToClone: Array<RepositoryToClone<Tag>>, codemods: Array<CodemodRunnerCodemod<Tag, C>>, options: {
18
+ workingDirectory: string;
19
+ pushChanges: boolean;
20
+ }): Promise<void>;
21
+ export declare function runCodemods<C extends Codemod = Codemod>(codemods: Array<C>, transformationPath: string, options?: RunCodemodOptions<C>): Promise<Record<string, Array<RunCodemodResult>>>;
22
+ export declare function runCodemod<C extends Codemod = Codemod>(codemod: C, transformationPath: string, options?: RunCodemodOptions<C>): Promise<Array<RunCodemodResult>>;
19
23
  export declare function traverseUp(node: SgNode<TypesMap, Kinds<TypesMap>>, until: (node: SgNode<TypesMap, Kinds<TypesMap>>) => boolean): types.Optional<SgNode<TypesMap, Kinds<TypesMap>>>;
20
24
  export declare function findAndReplaceConfigModifications(modifications: Modifications, config: Array<FindAndReplaceConfig>): Promise<Modifications>;
21
25
  export declare function findAndReplaceConfig(content: SgRoot<TypesMap>, lang: NapiLang, config: Array<FindAndReplaceConfig>): Promise<string>;
@@ -0,0 +1,10 @@
1
+ interface IBranch {
2
+ isSelected: boolean;
3
+ name: string;
4
+ }
5
+ declare class Branch implements IBranch {
6
+ readonly isSelected: boolean;
7
+ readonly name: string;
8
+ constructor(params: IBranch);
9
+ }
10
+ export default Branch;
@@ -0,0 +1,30 @@
1
+ import type { types } from '@kamaalio/kamaal';
2
+ import type Repository from './repository.js';
3
+ export declare class GitError extends Error {
4
+ readonly cause: types.Optional<unknown>;
5
+ readonly repository: Repository;
6
+ constructor(message: string, repository: Repository, options?: {
7
+ cause: unknown;
8
+ });
9
+ }
10
+ export declare class CloneError extends GitError {
11
+ constructor(repository: Repository);
12
+ }
13
+ export declare class CheckoutError extends GitError {
14
+ constructor(repository: Repository, branchName: string);
15
+ }
16
+ export declare class GetMainBranchError extends GitError {
17
+ constructor(repository: Repository, options?: {
18
+ cause: unknown;
19
+ });
20
+ }
21
+ export declare class PushError extends GitError {
22
+ constructor(repository: Repository, message: string, options?: {
23
+ cause: unknown;
24
+ });
25
+ }
26
+ export declare class RebaseError extends GitError {
27
+ constructor(repository: Repository, options?: {
28
+ cause: unknown;
29
+ });
30
+ }
@@ -0,0 +1,3 @@
1
+ export { cloneRepositories, cloneRepository } from './utils.js';
2
+ export { GitError } from './errors.js';
3
+ export { default as Repository } from './repository.js';
@@ -0,0 +1,41 @@
1
+ import { type ResultAsync, type Result } from 'neverthrow';
2
+ import { type types } from '@kamaalio/kamaal';
3
+ import { GitError } from './errors.js';
4
+ import Branch from './branch.js';
5
+ interface IRepository {
6
+ name: string;
7
+ address: string;
8
+ path: string;
9
+ }
10
+ declare class Repository implements IRepository {
11
+ readonly name: string;
12
+ readonly address: string;
13
+ readonly path: string;
14
+ private currentBranch;
15
+ private constructor();
16
+ clone: () => Promise<Result<void, GitError>>;
17
+ commit: (message: string) => Promise<void>;
18
+ push: () => Promise<Result<void, GitError>>;
19
+ resetBranch: (workingBranchName: string) => Promise<Result<void, GitError>>;
20
+ updateBranchToLatestMain: (workingBranchName: string) => Promise<Result<void, GitError>>;
21
+ deleteBranch: (branchName: string) => ResultAsync<unknown, unknown>;
22
+ prepareForUpdate: (workingBranchName: string) => Promise<Result<Repository, GitError>>;
23
+ getMainBranch: () => Promise<Result<Branch, GitError>>;
24
+ getRemoteName: () => Promise<types.Optional<string>>;
25
+ checkoutBranch: (branchName: string, options?: {
26
+ forceCreateNew: boolean;
27
+ }) => Promise<Result<Branch, GitError>>;
28
+ getSelectedBranch: () => Promise<types.Optional<Branch>>;
29
+ getRemoteBranches: () => Promise<Array<Branch>>;
30
+ getBranches: () => Promise<Array<Branch>>;
31
+ private get exec();
32
+ private getCurrentBranch;
33
+ private setCurrentBranch;
34
+ private getMainBranchName;
35
+ static fromAddressAndCwd: ({ address, cwd }: {
36
+ address: string;
37
+ cwd: string;
38
+ }) => types.Optional<Repository>;
39
+ private static getRepoNameFromRepoAddress;
40
+ }
41
+ export default Repository;
@@ -0,0 +1,18 @@
1
+ import z from 'zod';
2
+ export declare const DefaultBranchRefSchema: z.ZodObject<{
3
+ defaultBranchRef: z.ZodObject<{
4
+ name: z.ZodString;
5
+ }, "strip", z.ZodTypeAny, {
6
+ name: string;
7
+ }, {
8
+ name: string;
9
+ }>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ defaultBranchRef: {
12
+ name: string;
13
+ };
14
+ }, {
15
+ defaultBranchRef: {
16
+ name: string;
17
+ };
18
+ }>;
@@ -0,0 +1,4 @@
1
+ import { type types } from '@kamaalio/kamaal';
2
+ import Repository from './repository.js';
3
+ export declare function cloneRepositories(repoAddresses: Array<string>, location: string): Promise<Array<Repository>>;
4
+ export declare function cloneRepository(repoAddress: string, location: string): Promise<types.Optional<Repository>>;
@@ -0,0 +1 @@
1
+ export { makePullRequestsForCodemodResults, makePullRequestsForCodemodResult } from './utils.js';
@@ -0,0 +1,11 @@
1
+ import type { Result } from 'neverthrow';
2
+ import type { Repository } from '../git/index.js';
3
+ import type { Codemod, CodemodRunnerCodemod } from '../codemods/index.js';
4
+ export declare function makePullRequestsForCodemodResults<Tag = string, C extends Codemod = Codemod>(codemods: Array<CodemodRunnerCodemod<Tag, C>>, codemodResults: Record<string, Array<Result<{
5
+ hasChanges: boolean;
6
+ content: string;
7
+ }, Error>>>, repositories: Array<Repository>): Promise<void>;
8
+ export declare function makePullRequestsForCodemodResult<Tag = string, C extends Codemod = Codemod>(codemod: CodemodRunnerCodemod<Tag, C>, codemodResult: Array<Result<{
9
+ hasChanges: boolean;
10
+ content: string;
11
+ }, Error>>, repositories: Array<Repository>): Promise<void>;
package/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ var __webpack_require__ = {};
33
33
  var __webpack_exports__ = {};
34
34
  __webpack_require__.r(__webpack_exports__);
35
35
  __webpack_require__.d(__webpack_exports__, {
36
+ traverseUp: ()=>traverseUp,
36
37
  findAndReplace: ()=>findAndReplace,
37
38
  findAndReplaceConfig: ()=>findAndReplaceConfig,
38
39
  commitEditModifications: ()=>commitEditModifications,
@@ -40,7 +41,7 @@ __webpack_require__.d(__webpack_exports__, {
40
41
  findAndReplaceConfigModifications: ()=>findAndReplaceConfigModifications,
41
42
  findAndReplaceEdits: ()=>findAndReplaceEdits,
42
43
  runCodemods: ()=>runCodemods,
43
- traverseUp: ()=>traverseUp
44
+ runCodemodsOnProjects: ()=>runCodemodsOnProjects
44
45
  });
45
46
  const external_node_path_namespaceObject = require("node:path");
46
47
  var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
@@ -96,6 +97,419 @@ function groupBy(array, key) {
96
97
  return acc;
97
98
  }, {});
98
99
  }
100
+ const external_execa_namespaceObject = require("execa");
101
+ class GitError extends Error {
102
+ cause;
103
+ repository;
104
+ constructor(message, repository, options){
105
+ super(message);
106
+ this.repository = repository;
107
+ this.cause = options?.cause;
108
+ }
109
+ }
110
+ class CloneError extends GitError {
111
+ constructor(repository){
112
+ super(`Git clone failed for ${repository.address}`, repository);
113
+ }
114
+ }
115
+ class CheckoutError extends GitError {
116
+ constructor(repository, branchName){
117
+ super(`Git checkout failed for ${repository.address}, couldn't checkout to ${branchName}`, repository);
118
+ }
119
+ }
120
+ class GetMainBranchError extends GitError {
121
+ constructor(repository, options){
122
+ super(`Failed to get main branch for ${repository.address}`, repository, options);
123
+ }
124
+ }
125
+ class PushError extends GitError {
126
+ constructor(repository, message, options){
127
+ super(`Git push failed for ${repository.address}; message: ${message}`, repository, options);
128
+ }
129
+ }
130
+ class RebaseError extends GitError {
131
+ constructor(repository, options){
132
+ super(`Git rebase failed for ${repository.address}`, repository, options);
133
+ }
134
+ }
135
+ class Branch {
136
+ isSelected;
137
+ name;
138
+ constructor(params){
139
+ this.name = params.name;
140
+ this.isSelected = params.isSelected;
141
+ }
142
+ }
143
+ const git_branch = Branch;
144
+ const external_zod_namespaceObject = require("zod");
145
+ var external_zod_default = /*#__PURE__*/ __webpack_require__.n(external_zod_namespaceObject);
146
+ const DefaultBranchRefSchema = external_zod_default().object({
147
+ defaultBranchRef: external_zod_default().object({
148
+ name: external_zod_default().string().nonempty()
149
+ })
150
+ });
151
+ function tryCatch(callback) {
152
+ try {
153
+ return (0, external_neverthrow_namespaceObject.ok)(callback());
154
+ } catch (error) {
155
+ return (0, external_neverthrow_namespaceObject.err)(error);
156
+ }
157
+ }
158
+ function tryCatchAsync(callback) {
159
+ return external_neverthrow_namespaceObject.ResultAsync.fromPromise(callback(), (e)=>e);
160
+ }
161
+ function groupResults(results) {
162
+ return results.reduce((acc, result)=>{
163
+ if (result.isErr()) return {
164
+ ...acc,
165
+ failure: [
166
+ ...acc.failure,
167
+ result.error
168
+ ]
169
+ };
170
+ return {
171
+ ...acc,
172
+ success: [
173
+ ...acc.success,
174
+ result.value
175
+ ]
176
+ };
177
+ }, {
178
+ success: [],
179
+ failure: []
180
+ });
181
+ }
182
+ class Repository {
183
+ name;
184
+ address;
185
+ path;
186
+ currentBranch;
187
+ constructor(params){
188
+ this.name = params.name;
189
+ this.address = params.address;
190
+ this.path = params.path;
191
+ }
192
+ clone = async ()=>{
193
+ const cwd = this.path.split('/').slice(0, -1).join('/');
194
+ const exec = (0, external_execa_namespaceObject.$)({
195
+ cwd
196
+ });
197
+ const cloneResult = await exec`git clone ${this.address}`;
198
+ if (null != cloneResult.code && '0' !== cloneResult.code) return (0, external_neverthrow_namespaceObject.err)(new CloneError(this));
199
+ return (0, external_neverthrow_namespaceObject.ok)();
200
+ };
201
+ commit = async (message)=>{
202
+ await this.exec`git add .`;
203
+ await tryCatchAsync(()=>this.exec`git commit -m ${message} --no-verify`);
204
+ };
205
+ push = async ()=>{
206
+ const [remoteName, currentBranch, mainBranchNameResult] = await Promise.all([
207
+ this.getRemoteName(),
208
+ this.getCurrentBranch(),
209
+ this.getMainBranchName()
210
+ ]);
211
+ if (null == remoteName || null == currentBranch) return (0, external_neverthrow_namespaceObject.err)(new PushError(this, 'Failed to find remote name'));
212
+ if (mainBranchNameResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(mainBranchNameResult.error);
213
+ if (mainBranchNameResult.value === currentBranch.name) return (0, external_neverthrow_namespaceObject.err)(new PushError(this, "Can't push to main branch"));
214
+ await this.exec`git add .`;
215
+ await this.exec`git push --force --no-verify --set-upstream ${remoteName} ${currentBranch.name}`;
216
+ return (0, external_neverthrow_namespaceObject.ok)();
217
+ };
218
+ resetBranch = async (workingBranchName)=>{
219
+ await this.exec`git add .`;
220
+ await this.exec`git reset --hard`;
221
+ await this.exec`git fetch`;
222
+ return this.updateBranchToLatestMain(workingBranchName);
223
+ };
224
+ updateBranchToLatestMain = async (workingBranchName)=>{
225
+ const [remoteName, mainBranchNameResult] = await Promise.all([
226
+ this.getRemoteName(),
227
+ this.getMainBranchName()
228
+ ]);
229
+ if (mainBranchNameResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(new RebaseError(this, {
230
+ cause: mainBranchNameResult.error
231
+ }));
232
+ if (null == remoteName) return (0, external_neverthrow_namespaceObject.err)(new RebaseError(this));
233
+ const remoteBranches = await this.getRemoteBranches();
234
+ const existsInRemote = remoteBranches.some((branch)=>branch.name === workingBranchName);
235
+ const checkoutWorkingBranchResult = await this.checkoutBranch(workingBranchName);
236
+ if (checkoutWorkingBranchResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(checkoutWorkingBranchResult.error);
237
+ const rebaseResult = await tryCatchAsync(()=>this.exec`git rebase ${remoteName}/${mainBranchNameResult.value}`);
238
+ if (rebaseResult.isErr() || !existsInRemote) {
239
+ if (workingBranchName === mainBranchNameResult.value) return (0, external_neverthrow_namespaceObject.err)(new GitError('Checked out branch is main branch', this));
240
+ const checkoutResult = await this.checkoutBranch(mainBranchNameResult.value);
241
+ if (checkoutResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(checkoutResult.error);
242
+ await this.deleteBranch(workingBranchName);
243
+ const checkoutWorkingBranchResult = await this.checkoutBranch(workingBranchName, {
244
+ forceCreateNew: true
245
+ });
246
+ if (checkoutWorkingBranchResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(checkoutWorkingBranchResult.error);
247
+ const currentBranch = await this.getCurrentBranch();
248
+ if (null == currentBranch) return (0, external_neverthrow_namespaceObject.err)(new GitError('Failed to find current branch', this));
249
+ if (currentBranch.name === mainBranchNameResult.value) return (0, external_neverthrow_namespaceObject.err)(new GitError('Checked out branch is main branch', this));
250
+ }
251
+ return (0, external_neverthrow_namespaceObject.ok)();
252
+ };
253
+ deleteBranch = (branchName)=>tryCatchAsync(()=>this.exec`git branch -D ${branchName}`);
254
+ prepareForUpdate = async (workingBranchName)=>{
255
+ const checkoutBranchResult = await this.checkoutBranch(workingBranchName);
256
+ if (checkoutBranchResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(checkoutBranchResult.error);
257
+ const resetBranchResult = await this.resetBranch(workingBranchName);
258
+ if (resetBranchResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(resetBranchResult.error);
259
+ return (0, external_neverthrow_namespaceObject.ok)(this);
260
+ };
261
+ getMainBranch = async ()=>{
262
+ const [branches, mainBranchNameResult] = await Promise.all([
263
+ this.getBranches(),
264
+ this.getMainBranchName()
265
+ ]);
266
+ if (mainBranchNameResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(mainBranchNameResult.error);
267
+ const mainBranch = branches.find((branch)=>branch.name === mainBranchNameResult.value);
268
+ if (null == mainBranch) return (0, external_neverthrow_namespaceObject.err)(new GetMainBranchError(this));
269
+ return (0, external_neverthrow_namespaceObject.ok)(mainBranch);
270
+ };
271
+ getRemoteName = async ()=>{
272
+ const remoteResult = await this.exec`git remote`;
273
+ return remoteResult.stdout.split('\n')[0];
274
+ };
275
+ checkoutBranch = async (branchName, options)=>{
276
+ const currentBranch = await this.getCurrentBranch();
277
+ const forceCreateNew = options?.forceCreateNew ?? false;
278
+ if (null != currentBranch && currentBranch.name === branchName && !forceCreateNew) return (0, external_neverthrow_namespaceObject.ok)(currentBranch);
279
+ const branches = await this.getBranches();
280
+ const existingBranch = branches.find((branch)=>branch.name === branchName);
281
+ if (null != existingBranch && !forceCreateNew) {
282
+ const checkoutResult = await this.exec`git checkout ${branchName}`;
283
+ if (null != checkoutResult.code && '0' !== checkoutResult.code) return (0, external_neverthrow_namespaceObject.err)(new CheckoutError(this, branchName));
284
+ return (0, external_neverthrow_namespaceObject.ok)(existingBranch);
285
+ }
286
+ const checkoutResult = await this.exec`git checkout -b ${branchName}`;
287
+ if (null != checkoutResult.code && '0' !== checkoutResult.code) return (0, external_neverthrow_namespaceObject.err)(new CheckoutError(this, branchName));
288
+ const branch = new git_branch({
289
+ name: branchName,
290
+ isSelected: true
291
+ });
292
+ this.setCurrentBranch(branch);
293
+ return (0, external_neverthrow_namespaceObject.ok)(branch);
294
+ };
295
+ getSelectedBranch = async ()=>{
296
+ const currentBranch = await this.getCurrentBranch();
297
+ if (null != currentBranch) return currentBranch;
298
+ const branches = await this.getBranches();
299
+ const branch = branches.find((branch)=>branch.isSelected);
300
+ if (null == branch) return null;
301
+ this.setCurrentBranch(branch);
302
+ return branch;
303
+ };
304
+ getRemoteBranches = async ()=>{
305
+ const remoteHeadsResult = await this.exec`git ls-remote --heads`;
306
+ return kamaal_namespaceObject.arrays.compactMap(remoteHeadsResult.stdout.split('\n'), (line)=>{
307
+ const isValid = line.includes('refs/heads/');
308
+ if (!isValid) return null;
309
+ const name = line.trim().split('refs/heads/')[1];
310
+ if (!name) return null;
311
+ return new git_branch({
312
+ name,
313
+ isSelected: false
314
+ });
315
+ });
316
+ };
317
+ getBranches = async ()=>{
318
+ const branchResult = await this.exec`git branch`;
319
+ return branchResult.stdout.split('\n').map((branch)=>{
320
+ const name = branch.split('*').join('').trim();
321
+ const isSelected = branch.startsWith('*');
322
+ return new git_branch({
323
+ name,
324
+ isSelected
325
+ });
326
+ });
327
+ };
328
+ get exec() {
329
+ return (0, external_execa_namespaceObject.$)({
330
+ cwd: this.path
331
+ });
332
+ }
333
+ getCurrentBranch = async ()=>{
334
+ if (null != this.currentBranch) return this.currentBranch;
335
+ const branches = await this.getBranches();
336
+ return branches.find((branch)=>branch.isSelected);
337
+ };
338
+ setCurrentBranch = (branch)=>{
339
+ this.currentBranch = branch;
340
+ };
341
+ getMainBranchName = async ()=>{
342
+ const rawDefaultBranchRefResult = await this.exec`gh repo view --json defaultBranchRef`;
343
+ if (null != rawDefaultBranchRefResult.code && '0' !== rawDefaultBranchRefResult.code) return (0, external_neverthrow_namespaceObject.err)(new GetMainBranchError(this));
344
+ const parsedStdoutResult = tryCatch(()=>JSON.parse(rawDefaultBranchRefResult.stdout)).mapErr((e)=>new GetMainBranchError(this, {
345
+ cause: e
346
+ }));
347
+ if (parsedStdoutResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(parsedStdoutResult.error);
348
+ const defaultBranchRef = await DefaultBranchRefSchema.safeParseAsync(parsedStdoutResult.value);
349
+ if (defaultBranchRef.error) return (0, external_neverthrow_namespaceObject.err)(new GetMainBranchError(this, {
350
+ cause: defaultBranchRef.error
351
+ }));
352
+ return (0, external_neverthrow_namespaceObject.ok)(defaultBranchRef.data.defaultBranchRef.name);
353
+ };
354
+ static fromAddressAndCwd = ({ address, cwd })=>{
355
+ const name = Repository.getRepoNameFromRepoAddress(address);
356
+ if (null == name) return null;
357
+ return new Repository({
358
+ name,
359
+ address,
360
+ path: external_node_path_default().resolve(cwd, name)
361
+ });
362
+ };
363
+ static getRepoNameFromRepoAddress = (repoAddress)=>{
364
+ const repoAddressComponents = repoAddress.split('/');
365
+ return repoAddressComponents[repoAddressComponents.length - 1]?.split('.').slice(0, -1).join('.');
366
+ };
367
+ }
368
+ const git_repository = Repository;
369
+ async function cloneRepositories(repoAddresses, location) {
370
+ await (0, external_execa_namespaceObject.$)`mkdir -p ${location}`;
371
+ const exec = (0, external_execa_namespaceObject.$)({
372
+ cwd: location
373
+ });
374
+ const dedupedRepos = dedupeRepositoriesToClone(repoAddresses, location);
375
+ const existingRepositories = await getExistingRepositories(exec, repoAddresses);
376
+ const results = await Promise.all(dedupedRepos.map(cloneRepositoryInternal(existingRepositories)));
377
+ return kamaal_namespaceObject.arrays.compactMap(results, (result, index)=>{
378
+ const repo = dedupedRepos[index];
379
+ if (result.isErr()) {
380
+ console.error(result.error.message);
381
+ return null;
382
+ }
383
+ return repo;
384
+ });
385
+ }
386
+ async function getExistingRepositories(exec, repoAddresses) {
387
+ const lsResult = await exec`ls`;
388
+ const existingNames = new Set(lsResult.stdout.split('\n'));
389
+ return kamaal_namespaceObject.arrays.compactMap(repoAddresses, (address)=>{
390
+ const repository = git_repository.fromAddressAndCwd({
391
+ address,
392
+ cwd: lsResult.cwd
393
+ });
394
+ if (null == repository) return null;
395
+ if (!existingNames.has(repository.name)) return null;
396
+ return repository;
397
+ });
398
+ }
399
+ function cloneRepositoryInternal(existingRepositories) {
400
+ const existingRepositoryAddresses = new Set(existingRepositories.map((repo)=>repo.address));
401
+ return async (repository)=>{
402
+ if (existingRepositoryAddresses.has(repository.address)) return (0, external_neverthrow_namespaceObject.ok)();
403
+ return repository.clone();
404
+ };
405
+ }
406
+ function dedupeRepositoriesToClone(repoAddresses, cwd) {
407
+ const initialDedupeResult = {
408
+ result: [],
409
+ names: []
410
+ };
411
+ const dedupedRepos = repoAddresses.reduce((acc, repoAddress)=>{
412
+ const repository = git_repository.fromAddressAndCwd({
413
+ address: repoAddress,
414
+ cwd
415
+ });
416
+ if (null == repository) return acc;
417
+ if (acc.names.includes(repository.name)) return acc;
418
+ return {
419
+ result: [
420
+ ...acc.result,
421
+ repository
422
+ ],
423
+ names: [
424
+ ...acc.names,
425
+ repository.name
426
+ ]
427
+ };
428
+ }, initialDedupeResult).result;
429
+ return dedupedRepos;
430
+ }
431
+ async function makePullRequestsForCodemodResults(codemods, codemodResults, repositories) {
432
+ for (const [codemodName, codemodResult] of Object.entries(codemodResults)){
433
+ const codemod = codemods.find((c)=>c.name === codemodName);
434
+ if (null == codemod) throw new Error('Invariant found codemod should be present');
435
+ await makePullRequestsForCodemodResult(codemod, codemodResult, repositories);
436
+ }
437
+ }
438
+ async function makePullRequestsForCodemodResult(codemod, codemodResult, repositories) {
439
+ const { success } = groupResults(codemodResult);
440
+ const hasChanges = success.some((result)=>result.hasChanges);
441
+ if (!hasChanges) return void console.log(`\u{1F438} nothing transformed for '${codemod.name}'`);
442
+ await Promise.all(repositories.map(async (repository)=>{
443
+ await repository.commit(codemod.commitMessage);
444
+ const pushResult = await repository.push();
445
+ if (pushResult.isErr()) return void console.error(`\u{274C} Failed to push changes`, pushResult.error.message);
446
+ const pullRequestResult = await makePullRequest({
447
+ workingDirectory: repository.path,
448
+ title: codemod.commitMessage
449
+ });
450
+ if (pullRequestResult.isErr()) return void console.error(`\u{2705} already pushed transformation for`, repository.address);
451
+ console.log(`\u{2705} pull request created`, pullRequestResult.value.stdout);
452
+ }));
453
+ }
454
+ async function makePullRequest(params) {
455
+ return tryCatchAsync(()=>(0, external_execa_namespaceObject.$)({
456
+ cwd: params.workingDirectory
457
+ })`gh pr create --title ${params.title} --fill`);
458
+ }
459
+ async function runCodemodsOnProjects(repositoriesToClone, codemods, options) {
460
+ const clonedRepositories = await cloneRepositories(repositoriesToClone.map((repo)=>repo.address), options.workingDirectory);
461
+ console.log(`\u{1F5A8}\u{FE0F} cloned ${clonedRepositories.length} ${1 === clonedRepositories.length ? 'repository' : 'repositories'}`);
462
+ const mappingsByName = clonedRepositories.reduce((acc, repository)=>{
463
+ const tags = repositoriesToClone.find(({ address })=>address === repository.address)?.tags;
464
+ kamaal_namespaceObject.asserts.invariant(null != tags, 'Tags should be present');
465
+ return {
466
+ ...acc,
467
+ [repository.name]: {
468
+ repository,
469
+ tags
470
+ }
471
+ };
472
+ }, {});
473
+ const codemodResults = await runCodemodRunner(codemods, clonedRepositories, mappingsByName, options.workingDirectory);
474
+ if (options.pushChanges) await makePullRequestsForCodemodResults(codemods, codemodResults, clonedRepositories);
475
+ }
476
+ function codemodTargetFiltering(mappingsByName, failedRepositoryAddressesMappedByCodemodNames) {
477
+ return (filepath, codemod)=>{
478
+ const projectName = filepath.split('/')[0];
479
+ kamaal_namespaceObject.asserts.invariant(null != projectName, 'project name should be present');
480
+ const mapping = mappingsByName[projectName];
481
+ if (null == mapping) return false;
482
+ const failedRepositoryAddressesSet = failedRepositoryAddressesMappedByCodemodNames[codemod.name];
483
+ if (null == failedRepositoryAddressesSet || collectionIsEmpty(failedRepositoryAddressesSet)) return true;
484
+ return !failedRepositoryAddressesSet.has(mapping.repository.address) && (collectionIsEmpty(codemod.tags) || collectionIsEmpty(mapping.tags) || [
485
+ ...codemod.tags
486
+ ].some((tag)=>mapping.tags.has(tag)));
487
+ };
488
+ }
489
+ async function codemodPreCodemodRun(repositories, codemod) {
490
+ const preparationResults = await Promise.all(repositories.map((repo)=>repo.prepareForUpdate(codemod.name)));
491
+ const { failure: preparationFailures } = groupResults(preparationResults);
492
+ const failedRepositoryAddresses = preparationFailures.map((failure)=>failure.repository.address);
493
+ if (preparationFailures.length > 0) console.error(`\u{274C} failed to prepare [${failedRepositoryAddresses.join(', ')}] for '${codemod.name}'`);
494
+ return new Set(failedRepositoryAddresses);
495
+ }
496
+ async function codemodPostTransform(transformedContent) {
497
+ return transformedContent;
498
+ }
499
+ async function runCodemodRunner(codemods, repositories, mappingsByName, workingDirectory) {
500
+ const rootPaths = repositories.map((repository)=>repository.path);
501
+ const failedRepositoryAddressesMappedByCodemodNames = {};
502
+ return runCodemods(codemods, workingDirectory, {
503
+ rootPaths,
504
+ hooks: {
505
+ preCodemodRun: async (codemod)=>{
506
+ failedRepositoryAddressesMappedByCodemodNames[codemod.name] = await codemodPreCodemodRun(repositories, codemod);
507
+ },
508
+ targetFiltering: codemodTargetFiltering(mappingsByName, failedRepositoryAddressesMappedByCodemodNames),
509
+ postTransform: codemodPostTransform
510
+ }
511
+ });
512
+ }
99
513
  async function runCodemods(codemods, transformationPath, options) {
100
514
  const results = {};
101
515
  for (const codemod of codemods)results[codemod.name] = await runCodemod(codemod, transformationPath, options);
@@ -139,7 +553,7 @@ async function runCodemod(codemod, transformationPath, options) {
139
553
  hasChanges,
140
554
  content: modifiedContent,
141
555
  fullPath,
142
- root: filepath.split('/')[0]
556
+ root: external_node_path_default().resolve(transformationPath, filepath.split('/')[0])
143
557
  });
144
558
  } catch (error) {
145
559
  if (enableLogging) console.error(`\u{274C} '${codemod.name}' failed to parse file`, filepath, error);
@@ -309,6 +723,7 @@ exports.findAndReplaceConfigModifications = __webpack_exports__.findAndReplaceCo
309
723
  exports.findAndReplaceEdits = __webpack_exports__.findAndReplaceEdits;
310
724
  exports.runCodemod = __webpack_exports__.runCodemod;
311
725
  exports.runCodemods = __webpack_exports__.runCodemods;
726
+ exports.runCodemodsOnProjects = __webpack_exports__.runCodemodsOnProjects;
312
727
  exports.traverseUp = __webpack_exports__.traverseUp;
313
728
  for(var __webpack_i__ in __webpack_exports__)if (-1 === [
314
729
  "commitEditModifications",
@@ -318,6 +733,7 @@ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
318
733
  "findAndReplaceEdits",
319
734
  "runCodemod",
320
735
  "runCodemods",
736
+ "runCodemodsOnProjects",
321
737
  "traverseUp"
322
738
  ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
323
739
  Object.defineProperty(exports, '__esModule', {
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { runCodemods, runCodemod, commitEditModifications, findAndReplace, findAndReplaceEdits, findAndReplaceConfig, findAndReplaceConfigModifications, traverseUp, type Codemod, type Modifications, type FindAndReplaceConfig, type RunCodemodOkResult, type RunCodemodResult, } from './codemods/index.js';
1
+ export { runCodemods, runCodemod, commitEditModifications, findAndReplace, findAndReplaceEdits, findAndReplaceConfig, findAndReplaceConfigModifications, traverseUp, runCodemodsOnProjects, type Codemod, type Modifications, type FindAndReplaceConfig, type RunCodemodOkResult, type RunCodemodResult, type CodemodRunnerCodemod, type RepositoryToClone, } from './codemods/index.js';
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import node_path from "node:path";
2
2
  import promises from "node:fs/promises";
3
3
  import fast_glob from "fast-glob";
4
- import { err, ok } from "neverthrow";
4
+ import { ResultAsync, err, ok } from "neverthrow";
5
5
  import { Lang, parseAsync } from "@ast-grep/napi";
6
- import { arrays } from "@kamaalio/kamaal";
6
+ import { arrays, asserts } from "@kamaalio/kamaal";
7
+ import { $ } from "execa";
8
+ import zod from "zod";
7
9
  const JAVASCRIPT_EXTENSIONS = [
8
10
  '.js',
9
11
  '.cjs',
@@ -49,6 +51,416 @@ function groupBy(array, key) {
49
51
  return acc;
50
52
  }, {});
51
53
  }
54
+ class GitError extends Error {
55
+ cause;
56
+ repository;
57
+ constructor(message, repository, options){
58
+ super(message);
59
+ this.repository = repository;
60
+ this.cause = options?.cause;
61
+ }
62
+ }
63
+ class CloneError extends GitError {
64
+ constructor(repository){
65
+ super(`Git clone failed for ${repository.address}`, repository);
66
+ }
67
+ }
68
+ class CheckoutError extends GitError {
69
+ constructor(repository, branchName){
70
+ super(`Git checkout failed for ${repository.address}, couldn't checkout to ${branchName}`, repository);
71
+ }
72
+ }
73
+ class GetMainBranchError extends GitError {
74
+ constructor(repository, options){
75
+ super(`Failed to get main branch for ${repository.address}`, repository, options);
76
+ }
77
+ }
78
+ class PushError extends GitError {
79
+ constructor(repository, message, options){
80
+ super(`Git push failed for ${repository.address}; message: ${message}`, repository, options);
81
+ }
82
+ }
83
+ class RebaseError extends GitError {
84
+ constructor(repository, options){
85
+ super(`Git rebase failed for ${repository.address}`, repository, options);
86
+ }
87
+ }
88
+ class Branch {
89
+ isSelected;
90
+ name;
91
+ constructor(params){
92
+ this.name = params.name;
93
+ this.isSelected = params.isSelected;
94
+ }
95
+ }
96
+ const git_branch = Branch;
97
+ const DefaultBranchRefSchema = zod.object({
98
+ defaultBranchRef: zod.object({
99
+ name: zod.string().nonempty()
100
+ })
101
+ });
102
+ function tryCatch(callback) {
103
+ try {
104
+ return ok(callback());
105
+ } catch (error) {
106
+ return err(error);
107
+ }
108
+ }
109
+ function tryCatchAsync(callback) {
110
+ return ResultAsync.fromPromise(callback(), (e)=>e);
111
+ }
112
+ function groupResults(results) {
113
+ return results.reduce((acc, result)=>{
114
+ if (result.isErr()) return {
115
+ ...acc,
116
+ failure: [
117
+ ...acc.failure,
118
+ result.error
119
+ ]
120
+ };
121
+ return {
122
+ ...acc,
123
+ success: [
124
+ ...acc.success,
125
+ result.value
126
+ ]
127
+ };
128
+ }, {
129
+ success: [],
130
+ failure: []
131
+ });
132
+ }
133
+ class Repository {
134
+ name;
135
+ address;
136
+ path;
137
+ currentBranch;
138
+ constructor(params){
139
+ this.name = params.name;
140
+ this.address = params.address;
141
+ this.path = params.path;
142
+ }
143
+ clone = async ()=>{
144
+ const cwd = this.path.split('/').slice(0, -1).join('/');
145
+ const exec = $({
146
+ cwd
147
+ });
148
+ const cloneResult = await exec`git clone ${this.address}`;
149
+ if (null != cloneResult.code && '0' !== cloneResult.code) return err(new CloneError(this));
150
+ return ok();
151
+ };
152
+ commit = async (message)=>{
153
+ await this.exec`git add .`;
154
+ await tryCatchAsync(()=>this.exec`git commit -m ${message} --no-verify`);
155
+ };
156
+ push = async ()=>{
157
+ const [remoteName, currentBranch, mainBranchNameResult] = await Promise.all([
158
+ this.getRemoteName(),
159
+ this.getCurrentBranch(),
160
+ this.getMainBranchName()
161
+ ]);
162
+ if (null == remoteName || null == currentBranch) return err(new PushError(this, 'Failed to find remote name'));
163
+ if (mainBranchNameResult.isErr()) return err(mainBranchNameResult.error);
164
+ if (mainBranchNameResult.value === currentBranch.name) return err(new PushError(this, "Can't push to main branch"));
165
+ await this.exec`git add .`;
166
+ await this.exec`git push --force --no-verify --set-upstream ${remoteName} ${currentBranch.name}`;
167
+ return ok();
168
+ };
169
+ resetBranch = async (workingBranchName)=>{
170
+ await this.exec`git add .`;
171
+ await this.exec`git reset --hard`;
172
+ await this.exec`git fetch`;
173
+ return this.updateBranchToLatestMain(workingBranchName);
174
+ };
175
+ updateBranchToLatestMain = async (workingBranchName)=>{
176
+ const [remoteName, mainBranchNameResult] = await Promise.all([
177
+ this.getRemoteName(),
178
+ this.getMainBranchName()
179
+ ]);
180
+ if (mainBranchNameResult.isErr()) return err(new RebaseError(this, {
181
+ cause: mainBranchNameResult.error
182
+ }));
183
+ if (null == remoteName) return err(new RebaseError(this));
184
+ const remoteBranches = await this.getRemoteBranches();
185
+ const existsInRemote = remoteBranches.some((branch)=>branch.name === workingBranchName);
186
+ const checkoutWorkingBranchResult = await this.checkoutBranch(workingBranchName);
187
+ if (checkoutWorkingBranchResult.isErr()) return err(checkoutWorkingBranchResult.error);
188
+ const rebaseResult = await tryCatchAsync(()=>this.exec`git rebase ${remoteName}/${mainBranchNameResult.value}`);
189
+ if (rebaseResult.isErr() || !existsInRemote) {
190
+ if (workingBranchName === mainBranchNameResult.value) return err(new GitError('Checked out branch is main branch', this));
191
+ const checkoutResult = await this.checkoutBranch(mainBranchNameResult.value);
192
+ if (checkoutResult.isErr()) return err(checkoutResult.error);
193
+ await this.deleteBranch(workingBranchName);
194
+ const checkoutWorkingBranchResult = await this.checkoutBranch(workingBranchName, {
195
+ forceCreateNew: true
196
+ });
197
+ if (checkoutWorkingBranchResult.isErr()) return err(checkoutWorkingBranchResult.error);
198
+ const currentBranch = await this.getCurrentBranch();
199
+ if (null == currentBranch) return err(new GitError('Failed to find current branch', this));
200
+ if (currentBranch.name === mainBranchNameResult.value) return err(new GitError('Checked out branch is main branch', this));
201
+ }
202
+ return ok();
203
+ };
204
+ deleteBranch = (branchName)=>tryCatchAsync(()=>this.exec`git branch -D ${branchName}`);
205
+ prepareForUpdate = async (workingBranchName)=>{
206
+ const checkoutBranchResult = await this.checkoutBranch(workingBranchName);
207
+ if (checkoutBranchResult.isErr()) return err(checkoutBranchResult.error);
208
+ const resetBranchResult = await this.resetBranch(workingBranchName);
209
+ if (resetBranchResult.isErr()) return err(resetBranchResult.error);
210
+ return ok(this);
211
+ };
212
+ getMainBranch = async ()=>{
213
+ const [branches, mainBranchNameResult] = await Promise.all([
214
+ this.getBranches(),
215
+ this.getMainBranchName()
216
+ ]);
217
+ if (mainBranchNameResult.isErr()) return err(mainBranchNameResult.error);
218
+ const mainBranch = branches.find((branch)=>branch.name === mainBranchNameResult.value);
219
+ if (null == mainBranch) return err(new GetMainBranchError(this));
220
+ return ok(mainBranch);
221
+ };
222
+ getRemoteName = async ()=>{
223
+ const remoteResult = await this.exec`git remote`;
224
+ return remoteResult.stdout.split('\n')[0];
225
+ };
226
+ checkoutBranch = async (branchName, options)=>{
227
+ const currentBranch = await this.getCurrentBranch();
228
+ const forceCreateNew = options?.forceCreateNew ?? false;
229
+ if (null != currentBranch && currentBranch.name === branchName && !forceCreateNew) return ok(currentBranch);
230
+ const branches = await this.getBranches();
231
+ const existingBranch = branches.find((branch)=>branch.name === branchName);
232
+ if (null != existingBranch && !forceCreateNew) {
233
+ const checkoutResult = await this.exec`git checkout ${branchName}`;
234
+ if (null != checkoutResult.code && '0' !== checkoutResult.code) return err(new CheckoutError(this, branchName));
235
+ return ok(existingBranch);
236
+ }
237
+ const checkoutResult = await this.exec`git checkout -b ${branchName}`;
238
+ if (null != checkoutResult.code && '0' !== checkoutResult.code) return err(new CheckoutError(this, branchName));
239
+ const branch = new git_branch({
240
+ name: branchName,
241
+ isSelected: true
242
+ });
243
+ this.setCurrentBranch(branch);
244
+ return ok(branch);
245
+ };
246
+ getSelectedBranch = async ()=>{
247
+ const currentBranch = await this.getCurrentBranch();
248
+ if (null != currentBranch) return currentBranch;
249
+ const branches = await this.getBranches();
250
+ const branch = branches.find((branch)=>branch.isSelected);
251
+ if (null == branch) return null;
252
+ this.setCurrentBranch(branch);
253
+ return branch;
254
+ };
255
+ getRemoteBranches = async ()=>{
256
+ const remoteHeadsResult = await this.exec`git ls-remote --heads`;
257
+ return arrays.compactMap(remoteHeadsResult.stdout.split('\n'), (line)=>{
258
+ const isValid = line.includes('refs/heads/');
259
+ if (!isValid) return null;
260
+ const name = line.trim().split('refs/heads/')[1];
261
+ if (!name) return null;
262
+ return new git_branch({
263
+ name,
264
+ isSelected: false
265
+ });
266
+ });
267
+ };
268
+ getBranches = async ()=>{
269
+ const branchResult = await this.exec`git branch`;
270
+ return branchResult.stdout.split('\n').map((branch)=>{
271
+ const name = branch.split('*').join('').trim();
272
+ const isSelected = branch.startsWith('*');
273
+ return new git_branch({
274
+ name,
275
+ isSelected
276
+ });
277
+ });
278
+ };
279
+ get exec() {
280
+ return $({
281
+ cwd: this.path
282
+ });
283
+ }
284
+ getCurrentBranch = async ()=>{
285
+ if (null != this.currentBranch) return this.currentBranch;
286
+ const branches = await this.getBranches();
287
+ return branches.find((branch)=>branch.isSelected);
288
+ };
289
+ setCurrentBranch = (branch)=>{
290
+ this.currentBranch = branch;
291
+ };
292
+ getMainBranchName = async ()=>{
293
+ const rawDefaultBranchRefResult = await this.exec`gh repo view --json defaultBranchRef`;
294
+ if (null != rawDefaultBranchRefResult.code && '0' !== rawDefaultBranchRefResult.code) return err(new GetMainBranchError(this));
295
+ const parsedStdoutResult = tryCatch(()=>JSON.parse(rawDefaultBranchRefResult.stdout)).mapErr((e)=>new GetMainBranchError(this, {
296
+ cause: e
297
+ }));
298
+ if (parsedStdoutResult.isErr()) return err(parsedStdoutResult.error);
299
+ const defaultBranchRef = await DefaultBranchRefSchema.safeParseAsync(parsedStdoutResult.value);
300
+ if (defaultBranchRef.error) return err(new GetMainBranchError(this, {
301
+ cause: defaultBranchRef.error
302
+ }));
303
+ return ok(defaultBranchRef.data.defaultBranchRef.name);
304
+ };
305
+ static fromAddressAndCwd = ({ address, cwd })=>{
306
+ const name = Repository.getRepoNameFromRepoAddress(address);
307
+ if (null == name) return null;
308
+ return new Repository({
309
+ name,
310
+ address,
311
+ path: node_path.resolve(cwd, name)
312
+ });
313
+ };
314
+ static getRepoNameFromRepoAddress = (repoAddress)=>{
315
+ const repoAddressComponents = repoAddress.split('/');
316
+ return repoAddressComponents[repoAddressComponents.length - 1]?.split('.').slice(0, -1).join('.');
317
+ };
318
+ }
319
+ const git_repository = Repository;
320
+ async function cloneRepositories(repoAddresses, location) {
321
+ await $`mkdir -p ${location}`;
322
+ const exec = $({
323
+ cwd: location
324
+ });
325
+ const dedupedRepos = dedupeRepositoriesToClone(repoAddresses, location);
326
+ const existingRepositories = await getExistingRepositories(exec, repoAddresses);
327
+ const results = await Promise.all(dedupedRepos.map(cloneRepositoryInternal(existingRepositories)));
328
+ return arrays.compactMap(results, (result, index)=>{
329
+ const repo = dedupedRepos[index];
330
+ if (result.isErr()) {
331
+ console.error(result.error.message);
332
+ return null;
333
+ }
334
+ return repo;
335
+ });
336
+ }
337
+ async function getExistingRepositories(exec, repoAddresses) {
338
+ const lsResult = await exec`ls`;
339
+ const existingNames = new Set(lsResult.stdout.split('\n'));
340
+ return arrays.compactMap(repoAddresses, (address)=>{
341
+ const repository = git_repository.fromAddressAndCwd({
342
+ address,
343
+ cwd: lsResult.cwd
344
+ });
345
+ if (null == repository) return null;
346
+ if (!existingNames.has(repository.name)) return null;
347
+ return repository;
348
+ });
349
+ }
350
+ function cloneRepositoryInternal(existingRepositories) {
351
+ const existingRepositoryAddresses = new Set(existingRepositories.map((repo)=>repo.address));
352
+ return async (repository)=>{
353
+ if (existingRepositoryAddresses.has(repository.address)) return ok();
354
+ return repository.clone();
355
+ };
356
+ }
357
+ function dedupeRepositoriesToClone(repoAddresses, cwd) {
358
+ const initialDedupeResult = {
359
+ result: [],
360
+ names: []
361
+ };
362
+ const dedupedRepos = repoAddresses.reduce((acc, repoAddress)=>{
363
+ const repository = git_repository.fromAddressAndCwd({
364
+ address: repoAddress,
365
+ cwd
366
+ });
367
+ if (null == repository) return acc;
368
+ if (acc.names.includes(repository.name)) return acc;
369
+ return {
370
+ result: [
371
+ ...acc.result,
372
+ repository
373
+ ],
374
+ names: [
375
+ ...acc.names,
376
+ repository.name
377
+ ]
378
+ };
379
+ }, initialDedupeResult).result;
380
+ return dedupedRepos;
381
+ }
382
+ async function makePullRequestsForCodemodResults(codemods, codemodResults, repositories) {
383
+ for (const [codemodName, codemodResult] of Object.entries(codemodResults)){
384
+ const codemod = codemods.find((c)=>c.name === codemodName);
385
+ if (null == codemod) throw new Error('Invariant found codemod should be present');
386
+ await makePullRequestsForCodemodResult(codemod, codemodResult, repositories);
387
+ }
388
+ }
389
+ async function makePullRequestsForCodemodResult(codemod, codemodResult, repositories) {
390
+ const { success } = groupResults(codemodResult);
391
+ const hasChanges = success.some((result)=>result.hasChanges);
392
+ if (!hasChanges) return void console.log(`\u{1F438} nothing transformed for '${codemod.name}'`);
393
+ await Promise.all(repositories.map(async (repository)=>{
394
+ await repository.commit(codemod.commitMessage);
395
+ const pushResult = await repository.push();
396
+ if (pushResult.isErr()) return void console.error(`\u{274C} Failed to push changes`, pushResult.error.message);
397
+ const pullRequestResult = await makePullRequest({
398
+ workingDirectory: repository.path,
399
+ title: codemod.commitMessage
400
+ });
401
+ if (pullRequestResult.isErr()) return void console.error(`\u{2705} already pushed transformation for`, repository.address);
402
+ console.log(`\u{2705} pull request created`, pullRequestResult.value.stdout);
403
+ }));
404
+ }
405
+ async function makePullRequest(params) {
406
+ return tryCatchAsync(()=>$({
407
+ cwd: params.workingDirectory
408
+ })`gh pr create --title ${params.title} --fill`);
409
+ }
410
+ async function runCodemodsOnProjects(repositoriesToClone, codemods, options) {
411
+ const clonedRepositories = await cloneRepositories(repositoriesToClone.map((repo)=>repo.address), options.workingDirectory);
412
+ console.log(`\u{1F5A8}\u{FE0F} cloned ${clonedRepositories.length} ${1 === clonedRepositories.length ? 'repository' : 'repositories'}`);
413
+ const mappingsByName = clonedRepositories.reduce((acc, repository)=>{
414
+ const tags = repositoriesToClone.find(({ address })=>address === repository.address)?.tags;
415
+ asserts.invariant(null != tags, 'Tags should be present');
416
+ return {
417
+ ...acc,
418
+ [repository.name]: {
419
+ repository,
420
+ tags
421
+ }
422
+ };
423
+ }, {});
424
+ const codemodResults = await runCodemodRunner(codemods, clonedRepositories, mappingsByName, options.workingDirectory);
425
+ if (options.pushChanges) await makePullRequestsForCodemodResults(codemods, codemodResults, clonedRepositories);
426
+ }
427
+ function codemodTargetFiltering(mappingsByName, failedRepositoryAddressesMappedByCodemodNames) {
428
+ return (filepath, codemod)=>{
429
+ const projectName = filepath.split('/')[0];
430
+ asserts.invariant(null != projectName, 'project name should be present');
431
+ const mapping = mappingsByName[projectName];
432
+ if (null == mapping) return false;
433
+ const failedRepositoryAddressesSet = failedRepositoryAddressesMappedByCodemodNames[codemod.name];
434
+ if (null == failedRepositoryAddressesSet || collectionIsEmpty(failedRepositoryAddressesSet)) return true;
435
+ return !failedRepositoryAddressesSet.has(mapping.repository.address) && (collectionIsEmpty(codemod.tags) || collectionIsEmpty(mapping.tags) || [
436
+ ...codemod.tags
437
+ ].some((tag)=>mapping.tags.has(tag)));
438
+ };
439
+ }
440
+ async function codemodPreCodemodRun(repositories, codemod) {
441
+ const preparationResults = await Promise.all(repositories.map((repo)=>repo.prepareForUpdate(codemod.name)));
442
+ const { failure: preparationFailures } = groupResults(preparationResults);
443
+ const failedRepositoryAddresses = preparationFailures.map((failure)=>failure.repository.address);
444
+ if (preparationFailures.length > 0) console.error(`\u{274C} failed to prepare [${failedRepositoryAddresses.join(', ')}] for '${codemod.name}'`);
445
+ return new Set(failedRepositoryAddresses);
446
+ }
447
+ async function codemodPostTransform(transformedContent) {
448
+ return transformedContent;
449
+ }
450
+ async function runCodemodRunner(codemods, repositories, mappingsByName, workingDirectory) {
451
+ const rootPaths = repositories.map((repository)=>repository.path);
452
+ const failedRepositoryAddressesMappedByCodemodNames = {};
453
+ return runCodemods(codemods, workingDirectory, {
454
+ rootPaths,
455
+ hooks: {
456
+ preCodemodRun: async (codemod)=>{
457
+ failedRepositoryAddressesMappedByCodemodNames[codemod.name] = await codemodPreCodemodRun(repositories, codemod);
458
+ },
459
+ targetFiltering: codemodTargetFiltering(mappingsByName, failedRepositoryAddressesMappedByCodemodNames),
460
+ postTransform: codemodPostTransform
461
+ }
462
+ });
463
+ }
52
464
  async function runCodemods(codemods, transformationPath, options) {
53
465
  const results = {};
54
466
  for (const codemod of codemods)results[codemod.name] = await runCodemod(codemod, transformationPath, options);
@@ -92,7 +504,7 @@ async function runCodemod(codemod, transformationPath, options) {
92
504
  hasChanges,
93
505
  content: modifiedContent,
94
506
  fullPath,
95
- root: filepath.split('/')[0]
507
+ root: node_path.resolve(transformationPath, filepath.split('/')[0])
96
508
  });
97
509
  } catch (error) {
98
510
  if (enableLogging) console.error(`\u{274C} '${codemod.name}' failed to parse file`, filepath, error);
@@ -255,4 +667,4 @@ function defaultedHooks(hooks) {
255
667
  preCodemodRun
256
668
  };
257
669
  }
258
- export { commitEditModifications, findAndReplace, findAndReplaceConfig, findAndReplaceConfigModifications, findAndReplaceEdits, runCodemod, runCodemods, traverseUp };
670
+ export { commitEditModifications, findAndReplace, findAndReplaceConfig, findAndReplaceConfigModifications, findAndReplaceEdits, runCodemod, runCodemods, runCodemodsOnProjects, traverseUp };
@@ -0,0 +1,7 @@
1
+ import { ResultAsync, type Result } from 'neverthrow';
2
+ export declare function tryCatch<T>(callback: () => T): Result<T, unknown>;
3
+ export declare function tryCatchAsync<T>(callback: () => Promise<T>): ResultAsync<T, unknown>;
4
+ export declare function groupResults<S, E>(results: Array<Result<S, E>>): {
5
+ success: Array<S>;
6
+ failure: Array<E>;
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kamaalio/codemod-kit",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "type": "module",
5
5
  "author": "Kamaal Farah",
6
6
  "license": "MIT",
@@ -21,8 +21,10 @@
21
21
  "dependencies": {
22
22
  "@ast-grep/napi": "^0.38.5",
23
23
  "@kamaalio/kamaal": "^0.7.8",
24
+ "execa": "^9.6.0",
24
25
  "fast-glob": "^3.3.3",
25
- "neverthrow": "^8.2.0"
26
+ "neverthrow": "^8.2.0",
27
+ "zod": "^3.25.67"
26
28
  },
27
29
  "devDependencies": {
28
30
  "@eslint/js": "^9.29.0",