@kamaalio/codemod-kit 0.0.1

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/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # Codemod kit
2
+
3
+ A toolkit to run codemods.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @kamaalio/codemod-kit
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { runCodemods } from '@kamaalio/codemod-kit';
15
+ import type { CodeMod } from '@kamaalio/codemod-kit';
16
+
17
+ const myCodemod: CodeMod = {
18
+ name: 'my-codemod',
19
+ languages: ['typescript'],
20
+ commitMessage: 'feat(codemod): my first codemod',
21
+ transformer: async (content, filename) => {
22
+ // ... transform the content
23
+ },
24
+ };
25
+
26
+ runCodemods([myCodemod], './src');
27
+ ```
28
+
29
+ ## API
30
+
31
+ ### `runCodemods(codemods, transformationPath, options?)`
32
+
33
+ Runs a list of codemods on a given path.
34
+
35
+ - `codemods`: An array of `Codemod` objects.
36
+ - `transformationPath`: The path to the directory to transform.
37
+ - `options`: Optional configuration for the run.
38
+
39
+ ### `runCodemod(codemod, transformationPath, globItems, options?)`
40
+
41
+ Runs a single codemod.
42
+
43
+ - `codemod`: A `Codemod` object.
44
+ - `transformationPath`: The path to the directory to transform.
45
+ - `globItems`: An array of file paths to transform.
46
+ - `options`: Optional configuration for the run.
47
+
48
+ ### `Codemod`
49
+
50
+ A codemod is defined by the `Codemod` type:
51
+
52
+ ```typescript
53
+ export type Codemod = {
54
+ name: string;
55
+ languages: Set<NapiLang> | Array<NapiLang>;
56
+ commitMessage: string;
57
+ transformer: (content: SgRoot<TypesMap> | string, filename?: Optional<string>) => Promise<Modifications>;
58
+ };
59
+ ```
60
+
61
+ - `name`: The name of the codemod.
62
+ - `languages`: The languages the codemod applies to.
63
+ - `commitMessage`: The commit message to use when applying the codemod.
64
+ - `transformer`: The function that transforms the code.
65
+
66
+ ### `Modifications`
67
+
68
+ The `transformer` function returns a `Modifications` object:
69
+
70
+ ```typescript
71
+ export type Modifications = {
72
+ ast: SgRoot<TypesMap>;
73
+ report: ModificationsReport;
74
+ lang: NapiLang;
75
+ filename: Optional<string>;
76
+ history: Array<SgRoot<TypesMap>>;
77
+ };
78
+ ```
79
+
80
+ - `ast`: The modified AST.
81
+ - `report`: A report of the changes.
82
+ - `lang`: The language of the file.
83
+ - `filename`: The name of the file.
84
+ - `history`: A history of the modifications.
85
+
86
+ ## Hooks
87
+
88
+ You can provide hooks to customize the codemod run:
89
+
90
+ ```typescript
91
+ type RunCodemodHooks = {
92
+ targetFiltering?: (filepath: string) => boolean;
93
+ preCodemodRun?: (codemod: Codemod) => Promise<void>;
94
+ postTransform?: (transformedContent: string) => Promise<string>;
95
+ };
96
+ ```
97
+
98
+ - `targetFiltering`: A function to filter the files to transform.
99
+ - `preCodemodRun`: A function to run before each codemod.
100
+ - `postTransform`: A function to run after each transformation.
101
+
102
+ ## Options
103
+
104
+ You can provide options to customize the codemod run:
105
+
106
+ ```typescript
107
+ type RunCodemodOptions = {
108
+ hooks?: RunCodemodHooks;
109
+ log?: boolean;
110
+ dry?: boolean;
111
+ };
112
+ ```
113
+
114
+ - `hooks`: The hooks to use.
115
+ - `log`: Whether to log the output.
116
+ - `dry`: Whether to run in dry mode (no changes are written to disk).
@@ -0,0 +1 @@
1
+ export declare const LANG_TO_EXTENSIONS_MAPPING: Partial<Record<string, Set<string>>>;
@@ -0,0 +1,2 @@
1
+ export { runCodemods, runCodemod } from './utils';
2
+ export type { Codemod, Modifications } from './types';
@@ -0,0 +1,20 @@
1
+ import type { SgRoot } from '@ast-grep/napi';
2
+ import type { NapiLang } from '@ast-grep/napi/types/lang.js';
3
+ import type { TypesMap } from '@ast-grep/napi/types/staticTypes.js';
4
+ import type { Optional } from '../utils/type-utils';
5
+ export type Codemod = {
6
+ name: string;
7
+ languages: Set<NapiLang> | Array<NapiLang>;
8
+ commitMessage: string;
9
+ transformer: (content: SgRoot<TypesMap> | string, filename?: Optional<string>) => Promise<Modifications>;
10
+ };
11
+ export type ModificationsReport = {
12
+ changesApplied: number;
13
+ };
14
+ export type Modifications = {
15
+ ast: SgRoot<TypesMap>;
16
+ report: ModificationsReport;
17
+ lang: NapiLang;
18
+ filename: Optional<string>;
19
+ history: Array<SgRoot<TypesMap>>;
20
+ };
@@ -0,0 +1,15 @@
1
+ import { type Result } from 'neverthrow';
2
+ import type { Codemod, Modifications } from './types';
3
+ type RunCodemodHooks = {
4
+ targetFiltering?: (filepath: string) => boolean;
5
+ preCodemodRun?: (codemod: Codemod) => Promise<void>;
6
+ postTransform?: (transformedContent: string) => Promise<string>;
7
+ };
8
+ type RunCodemodOptions = {
9
+ hooks?: RunCodemodHooks;
10
+ log?: boolean;
11
+ dry?: boolean;
12
+ };
13
+ export declare function runCodemods(codemods: Array<Codemod>, transformationPath: string, options?: RunCodemodOptions): Promise<Record<string, Array<Result<Modifications, Error>>>>;
14
+ export declare function runCodemod(codemod: Codemod, transformationPath: string, globItems: Array<string>, options?: RunCodemodOptions): Promise<Array<Result<Modifications, Error>>>;
15
+ export {};
package/dist/index.cjs ADDED
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.n = (module)=>{
5
+ var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
6
+ __webpack_require__.d(getter, {
7
+ a: getter
8
+ });
9
+ return getter;
10
+ };
11
+ })();
12
+ (()=>{
13
+ __webpack_require__.d = (exports1, definition)=>{
14
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
15
+ enumerable: true,
16
+ get: definition[key]
17
+ });
18
+ };
19
+ })();
20
+ (()=>{
21
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
22
+ })();
23
+ (()=>{
24
+ __webpack_require__.r = (exports1)=>{
25
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
26
+ value: 'Module'
27
+ });
28
+ Object.defineProperty(exports1, '__esModule', {
29
+ value: true
30
+ });
31
+ };
32
+ })();
33
+ var __webpack_exports__ = {};
34
+ __webpack_require__.r(__webpack_exports__);
35
+ __webpack_require__.d(__webpack_exports__, {
36
+ runCodemod: ()=>runCodemod,
37
+ runCodemods: ()=>runCodemods
38
+ });
39
+ const external_node_path_namespaceObject = require("node:path");
40
+ var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
41
+ const promises_namespaceObject = require("node:fs/promises");
42
+ var promises_default = /*#__PURE__*/ __webpack_require__.n(promises_namespaceObject);
43
+ const external_fast_glob_namespaceObject = require("fast-glob");
44
+ var external_fast_glob_default = /*#__PURE__*/ __webpack_require__.n(external_fast_glob_namespaceObject);
45
+ const external_neverthrow_namespaceObject = require("neverthrow");
46
+ const napi_namespaceObject = require("@ast-grep/napi");
47
+ const LANG_TO_EXTENSIONS_MAPPING = {
48
+ [napi_namespaceObject.Lang.TypeScript.toLowerCase()]: new Set([
49
+ '.ts',
50
+ '.tsx',
51
+ '.js',
52
+ '.jsx',
53
+ '.cjs',
54
+ '.mjs',
55
+ '.mts'
56
+ ])
57
+ };
58
+ function getCollectionCount(collection) {
59
+ if (Array.isArray(collection)) return collection.length;
60
+ return collection.size;
61
+ }
62
+ function collectionIsEmpty(collection) {
63
+ return 0 === getCollectionCount(collection);
64
+ }
65
+ async function runCodemods(codemods, transformationPath, options) {
66
+ const globItems = await external_fast_glob_default().glob([
67
+ '**/*'
68
+ ], {
69
+ cwd: transformationPath
70
+ });
71
+ const results = {};
72
+ for (const codemod of codemods)results[codemod.name] = await runCodemod(codemod, transformationPath, globItems, options);
73
+ return results;
74
+ }
75
+ async function runCodemod(codemod, transformationPath, globItems, options) {
76
+ const { hooks, log: enableLogging, dry: runInDryMode } = defaultedOptions(options);
77
+ const extensions = new Set(Array.from(codemod.languages).reduce((acc, language)=>{
78
+ const mappedExtensions = LANG_TO_EXTENSIONS_MAPPING[language.toLowerCase()];
79
+ if (null == mappedExtensions) return acc;
80
+ return acc.concat(Array.from(mappedExtensions));
81
+ }, []));
82
+ const targets = globItems.filter((filepath)=>{
83
+ if (!hooks.targetFiltering(filepath)) return false;
84
+ const projectName = filepath.split('/')[0];
85
+ if (null == projectName) throw new Error('Invariant found, project name should be present');
86
+ return collectionIsEmpty(extensions) || extensions.has(external_node_path_default().extname(filepath));
87
+ });
88
+ if (0 === targets.length) return [];
89
+ if (enableLogging) console.log(`\u{1F9C9} '${codemod.name}' targeting ${targets.length} ${1 === targets.length ? 'file' : 'files'} to transform, chill and grab some mat\xe9`);
90
+ return Promise.all(targets.map(async (filepath)=>{
91
+ const fullPath = external_node_path_default().join(transformationPath, filepath);
92
+ try {
93
+ const content = await promises_default().readFile(fullPath, {
94
+ encoding: 'utf-8'
95
+ });
96
+ const modifications = await codemod.transformer(content, fullPath);
97
+ if (modifications.report.changesApplied > 0) {
98
+ const transformedContent = await hooks.postTransform(modifications.ast.root().text());
99
+ if (!runInDryMode) await promises_default().writeFile(fullPath, transformedContent);
100
+ if (enableLogging) console.log(`\u{1F680} finished '${codemod.name}'`, {
101
+ filename: filepath,
102
+ report: modifications.report
103
+ });
104
+ }
105
+ return (0, external_neverthrow_namespaceObject.ok)(modifications);
106
+ } catch (error) {
107
+ if (enableLogging) console.error(`\u{274C} '${codemod.name}' failed to parse file`, filepath, error);
108
+ return (0, external_neverthrow_namespaceObject.err)(error);
109
+ }
110
+ }));
111
+ }
112
+ function defaultedOptions(options) {
113
+ return {
114
+ hooks: defaultedHooks(options?.hooks),
115
+ log: options?.log ?? true,
116
+ dry: options?.dry ?? false
117
+ };
118
+ }
119
+ function defaultedHooks(hooks) {
120
+ const targetFiltering = hooks?.targetFiltering ?? (()=>true);
121
+ const postTransform = hooks?.postTransform ?? (async (content)=>content);
122
+ const preCodemodRun = hooks?.preCodemodRun ?? (async ()=>{});
123
+ return {
124
+ targetFiltering,
125
+ postTransform,
126
+ preCodemodRun
127
+ };
128
+ }
129
+ exports.runCodemod = __webpack_exports__.runCodemod;
130
+ exports.runCodemods = __webpack_exports__.runCodemods;
131
+ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
132
+ "runCodemod",
133
+ "runCodemods"
134
+ ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
135
+ Object.defineProperty(exports, '__esModule', {
136
+ value: true
137
+ });
@@ -0,0 +1 @@
1
+ export { runCodemods, runCodemod, type Codemod } from './codemods';
package/dist/index.js ADDED
@@ -0,0 +1,88 @@
1
+ import node_path from "node:path";
2
+ import promises from "node:fs/promises";
3
+ import fast_glob from "fast-glob";
4
+ import { err, ok } from "neverthrow";
5
+ import { Lang } from "@ast-grep/napi";
6
+ const LANG_TO_EXTENSIONS_MAPPING = {
7
+ [Lang.TypeScript.toLowerCase()]: new Set([
8
+ '.ts',
9
+ '.tsx',
10
+ '.js',
11
+ '.jsx',
12
+ '.cjs',
13
+ '.mjs',
14
+ '.mts'
15
+ ])
16
+ };
17
+ function getCollectionCount(collection) {
18
+ if (Array.isArray(collection)) return collection.length;
19
+ return collection.size;
20
+ }
21
+ function collectionIsEmpty(collection) {
22
+ return 0 === getCollectionCount(collection);
23
+ }
24
+ async function runCodemods(codemods, transformationPath, options) {
25
+ const globItems = await fast_glob.glob([
26
+ '**/*'
27
+ ], {
28
+ cwd: transformationPath
29
+ });
30
+ const results = {};
31
+ for (const codemod of codemods)results[codemod.name] = await runCodemod(codemod, transformationPath, globItems, options);
32
+ return results;
33
+ }
34
+ async function runCodemod(codemod, transformationPath, globItems, options) {
35
+ const { hooks, log: enableLogging, dry: runInDryMode } = defaultedOptions(options);
36
+ const extensions = new Set(Array.from(codemod.languages).reduce((acc, language)=>{
37
+ const mappedExtensions = LANG_TO_EXTENSIONS_MAPPING[language.toLowerCase()];
38
+ if (null == mappedExtensions) return acc;
39
+ return acc.concat(Array.from(mappedExtensions));
40
+ }, []));
41
+ const targets = globItems.filter((filepath)=>{
42
+ if (!hooks.targetFiltering(filepath)) return false;
43
+ const projectName = filepath.split('/')[0];
44
+ if (null == projectName) throw new Error('Invariant found, project name should be present');
45
+ return collectionIsEmpty(extensions) || extensions.has(node_path.extname(filepath));
46
+ });
47
+ if (0 === targets.length) return [];
48
+ if (enableLogging) console.log(`\u{1F9C9} '${codemod.name}' targeting ${targets.length} ${1 === targets.length ? 'file' : 'files'} to transform, chill and grab some mat\xe9`);
49
+ return Promise.all(targets.map(async (filepath)=>{
50
+ const fullPath = node_path.join(transformationPath, filepath);
51
+ try {
52
+ const content = await promises.readFile(fullPath, {
53
+ encoding: 'utf-8'
54
+ });
55
+ const modifications = await codemod.transformer(content, fullPath);
56
+ if (modifications.report.changesApplied > 0) {
57
+ const transformedContent = await hooks.postTransform(modifications.ast.root().text());
58
+ if (!runInDryMode) await promises.writeFile(fullPath, transformedContent);
59
+ if (enableLogging) console.log(`\u{1F680} finished '${codemod.name}'`, {
60
+ filename: filepath,
61
+ report: modifications.report
62
+ });
63
+ }
64
+ return ok(modifications);
65
+ } catch (error) {
66
+ if (enableLogging) console.error(`\u{274C} '${codemod.name}' failed to parse file`, filepath, error);
67
+ return err(error);
68
+ }
69
+ }));
70
+ }
71
+ function defaultedOptions(options) {
72
+ return {
73
+ hooks: defaultedHooks(options?.hooks),
74
+ log: options?.log ?? true,
75
+ dry: options?.dry ?? false
76
+ };
77
+ }
78
+ function defaultedHooks(hooks) {
79
+ const targetFiltering = hooks?.targetFiltering ?? (()=>true);
80
+ const postTransform = hooks?.postTransform ?? (async (content)=>content);
81
+ const preCodemodRun = hooks?.preCodemodRun ?? (async ()=>{});
82
+ return {
83
+ targetFiltering,
84
+ postTransform,
85
+ preCodemodRun
86
+ };
87
+ }
88
+ export { runCodemod, runCodemods };
@@ -0,0 +1,3 @@
1
+ type AnyCollection = Array<unknown> | Set<unknown>;
2
+ export declare function collectionIsEmpty<T extends AnyCollection>(collection: T): boolean;
3
+ export {};
@@ -0,0 +1 @@
1
+ export type Optional<T> = T | undefined | null;
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@kamaalio/codemod-kit",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "author": "Kamaal Farah",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js",
10
+ "require": "./dist/index.cjs"
11
+ }
12
+ },
13
+ "main": "./dist/index.cjs",
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@ast-grep/napi": "^0.38.5",
20
+ "fast-glob": "^3.3.3",
21
+ "neverthrow": "^8.2.0"
22
+ },
23
+ "devDependencies": {
24
+ "@eslint/js": "^9.29.0",
25
+ "@kamaalio/prettier-config": "^0.1.2",
26
+ "@rslib/core": "^0.10.2",
27
+ "@types/node": "^22.15.32",
28
+ "@vitest/coverage-v8": "3.2.4",
29
+ "eslint": "^9.29.0",
30
+ "globals": "^16.2.0",
31
+ "husky": "^9.1.7",
32
+ "lint-staged": "^16.1.2",
33
+ "prettier": "^3.5.3",
34
+ "tsx": "^4.20.3",
35
+ "typescript": "^5.8.3",
36
+ "typescript-eslint": "^8.34.1",
37
+ "vitest": "^3.2.4"
38
+ },
39
+ "lint-staged": {
40
+ "**/*": "prettier --write --ignore-unknown"
41
+ },
42
+ "prettier": "@kamaalio/prettier-config",
43
+ "scripts": {
44
+ "build": "rslib build",
45
+ "build:clean": "rm -rf dist tsconfig.tsbuildinfo && pnpm run build",
46
+ "dev": "rslib build --watch",
47
+ "format": "prettier --write .",
48
+ "format:check": "prettier . --check",
49
+ "lint": "eslint .",
50
+ "test": "vitest run",
51
+ "test:cov": "vitest run --coverage",
52
+ "type-check": "tsc --noEmit"
53
+ }
54
+ }