ts-codemod-lib 1.0.1 → 1.1.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.
Files changed (49) hide show
  1. package/README.md +428 -6
  2. package/dist/cmd/append-as-const.d.mts +3 -0
  3. package/dist/cmd/append-as-const.d.mts.map +1 -0
  4. package/dist/cmd/append-as-const.mjs +138 -0
  5. package/dist/cmd/append-as-const.mjs.map +1 -0
  6. package/dist/cmd/convert-interface-to-type.d.mts +3 -0
  7. package/dist/cmd/convert-interface-to-type.d.mts.map +1 -0
  8. package/dist/cmd/convert-interface-to-type.mjs +138 -0
  9. package/dist/cmd/convert-interface-to-type.mjs.map +1 -0
  10. package/dist/cmd/convert-to-readonly.mjs +3 -2
  11. package/dist/cmd/convert-to-readonly.mjs.map +1 -1
  12. package/dist/cmd/replace-any-with-unknown.d.mts +3 -0
  13. package/dist/cmd/replace-any-with-unknown.d.mts.map +1 -0
  14. package/dist/cmd/replace-any-with-unknown.mjs +138 -0
  15. package/dist/cmd/replace-any-with-unknown.mjs.map +1 -0
  16. package/dist/cmd/replace-record-with-unknown-record.d.mts +3 -0
  17. package/dist/cmd/replace-record-with-unknown-record.d.mts.map +1 -0
  18. package/dist/cmd/replace-record-with-unknown-record.mjs +138 -0
  19. package/dist/cmd/replace-record-with-unknown-record.mjs.map +1 -0
  20. package/dist/entry-point.mjs +2 -0
  21. package/dist/entry-point.mjs.map +1 -1
  22. package/dist/functions/ast-transformers/append-as-const.d.mts +13 -0
  23. package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -0
  24. package/dist/functions/ast-transformers/append-as-const.mjs +98 -0
  25. package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -0
  26. package/dist/functions/ast-transformers/index.d.mts +2 -0
  27. package/dist/functions/ast-transformers/index.d.mts.map +1 -1
  28. package/dist/functions/ast-transformers/index.mjs +2 -0
  29. package/dist/functions/ast-transformers/index.mjs.map +1 -1
  30. package/dist/functions/ast-transformers/replace-any-with-unknown.d.mts +3 -0
  31. package/dist/functions/ast-transformers/replace-any-with-unknown.d.mts.map +1 -0
  32. package/dist/functions/ast-transformers/replace-any-with-unknown.mjs +38 -0
  33. package/dist/functions/ast-transformers/replace-any-with-unknown.mjs.map +1 -0
  34. package/dist/functions/index.mjs +2 -0
  35. package/dist/functions/index.mjs.map +1 -1
  36. package/dist/index.mjs +2 -0
  37. package/dist/index.mjs.map +1 -1
  38. package/package.json +61 -57
  39. package/src/cmd/append-as-const.mts +197 -0
  40. package/src/cmd/convert-interface-to-type.mts +197 -0
  41. package/src/cmd/convert-to-readonly.mts +3 -1
  42. package/src/cmd/replace-any-with-unknown.mts +197 -0
  43. package/src/cmd/replace-record-with-unknown-record.mts +197 -0
  44. package/src/functions/ast-transformers/append-as-const.mts +154 -0
  45. package/src/functions/ast-transformers/append-as-const.test.mts +339 -0
  46. package/src/functions/ast-transformers/convert-to-readonly-type.test.mts +3 -3
  47. package/src/functions/ast-transformers/index.mts +2 -0
  48. package/src/functions/ast-transformers/replace-any-with-unknown.mts +49 -0
  49. package/src/functions/ast-transformers/replace-any-with-unknown.test.mts +140 -0
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-await-in-loop */
3
+
4
+ import * as cmd from 'cmd-ts';
5
+ import dedent from 'dedent';
6
+ import { castMutable, Result, unknownToString } from 'ts-data-forge';
7
+ import 'ts-repo-utils';
8
+ import {
9
+ replaceAnyWithUnknownTransformer,
10
+ transformSourceCode,
11
+ } from '../functions/index.mjs';
12
+
13
+ const cmdDef = cmd.command({
14
+ name: 'replace-any-with-unknown-cli',
15
+ version: '1.0.0',
16
+ args: {
17
+ baseDir: cmd.positional({
18
+ type: cmd.string,
19
+ displayName: 'baseDir',
20
+ description: 'The base directory in which to perform the conversion',
21
+ }),
22
+ exclude: cmd.multioption({
23
+ long: 'exclude',
24
+ type: cmd.optional(cmd.array(cmd.string)),
25
+ description:
26
+ 'Glob patterns of files to exclude from the base directory (e.g., "src/generated/**/*.mts")',
27
+ }),
28
+ silent: cmd.flag({
29
+ long: 'silent',
30
+ type: cmd.optional(cmd.boolean),
31
+ description: 'If true, suppresses output messages (default: false)',
32
+ }),
33
+ },
34
+ handler: (args) => {
35
+ replaceAnyWithUnknownCLI({
36
+ baseDir: args.baseDir,
37
+ exclude: args.exclude ?? [],
38
+ silent: args.silent ?? false,
39
+ }).catch((error: unknown) => {
40
+ console.error('An error occurred:', error);
41
+
42
+ process.exit(1);
43
+ });
44
+ },
45
+ });
46
+
47
+ type Args = Readonly<{
48
+ baseDir: string;
49
+ exclude: readonly string[];
50
+ silent: boolean;
51
+ }>;
52
+
53
+ const replaceAnyWithUnknownCLI = async (
54
+ args: Args,
55
+ ): Promise<Result<undefined, undefined>> => {
56
+ const echoIfNotSilent = args.silent ? () => {} : echo;
57
+
58
+ const errorIfNotSilent = args.silent ? () => {} : console.error;
59
+
60
+ // Find all files matching the glob
61
+ const globResult = await glob(args.baseDir, {
62
+ ignore: castMutable(args.exclude),
63
+ });
64
+
65
+ if (Result.isErr(globResult)) {
66
+ errorIfNotSilent('Error finding files matching pattern:', globResult.value);
67
+
68
+ return Result.err(undefined);
69
+ }
70
+
71
+ const files = globResult.value;
72
+
73
+ if (files.length === 0) {
74
+ echoIfNotSilent('No files found matching pattern:', args.baseDir);
75
+
76
+ return Result.ok(undefined);
77
+ }
78
+
79
+ const { errorFiles, transformedCount, unchangedCount } = await transformFiles(
80
+ files,
81
+ args.silent,
82
+ );
83
+
84
+ const hr = '='.repeat(50);
85
+
86
+ echoIfNotSilent(dedent`
87
+ ${hr}
88
+ Summary:
89
+ ✅ Transformed: ${transformedCount}
90
+ ⏭️ Unchanged: ${unchangedCount}
91
+ ❌ Errors: ${errorFiles.length}
92
+ 📊 Total: ${files.length}
93
+ `);
94
+
95
+ if (errorFiles.length > 0) {
96
+ echoIfNotSilent('\nFiles with errors:');
97
+
98
+ for (const fileName of errorFiles) {
99
+ echoIfNotSilent(` - ${fileName}`);
100
+ }
101
+ }
102
+
103
+ echoIfNotSilent(hr);
104
+
105
+ if (errorFiles.length > 0) {
106
+ return Result.err(undefined);
107
+ }
108
+
109
+ return Result.ok(undefined);
110
+ };
111
+
112
+ const transformFiles = async (
113
+ filePaths: readonly string[],
114
+ silent: boolean,
115
+ ): Promise<
116
+ Readonly<{
117
+ transformedCount: number;
118
+ unchangedCount: number;
119
+ errorFiles: readonly string[];
120
+ }>
121
+ > => {
122
+ let mut_transformedCount: number = 0;
123
+
124
+ let mut_unchangedCount: number = 0;
125
+
126
+ const mut_errorFiles: string[] = [];
127
+
128
+ for (const filePath of filePaths) {
129
+ const result = await transformOneFile(filePath, silent);
130
+
131
+ if (Result.isOk(result)) {
132
+ switch (result.value) {
133
+ case 'transformed':
134
+ mut_transformedCount += 1;
135
+
136
+ break;
137
+
138
+ case 'unchanged':
139
+ mut_unchangedCount += 1;
140
+
141
+ break;
142
+ }
143
+ } else {
144
+ mut_errorFiles.push(path.basename(filePath));
145
+ }
146
+ }
147
+
148
+ return {
149
+ transformedCount: mut_transformedCount,
150
+ unchangedCount: mut_unchangedCount,
151
+ errorFiles: mut_errorFiles,
152
+ };
153
+ };
154
+
155
+ const transformOneFile = async (
156
+ filePath: string,
157
+ silent: boolean,
158
+ ): Promise<Result<'unchanged' | 'transformed', string>> => {
159
+ const echoIfNotSilent = silent ? () => {} : echo;
160
+
161
+ const errorIfNotSilent = silent ? () => {} : console.error;
162
+
163
+ const fileName = path.basename(filePath);
164
+
165
+ const isTsx = fileName.endsWith('.tsx') || fileName.endsWith('.jsx');
166
+
167
+ try {
168
+ const originalCode = await fs.readFile(filePath, 'utf8');
169
+
170
+ // Transform the code with all transformers
171
+ const transformedCode = transformSourceCode(originalCode, isTsx, [
172
+ replaceAnyWithUnknownTransformer(),
173
+ ]);
174
+
175
+ // Check if the code was actually changed
176
+ if (transformedCode === originalCode) {
177
+ echoIfNotSilent(`⏭️ ${fileName} - no changes needed`);
178
+
179
+ return Result.ok('unchanged');
180
+ } else {
181
+ // Write back the transformed code
182
+ await fs.writeFile(filePath, transformedCode, 'utf8');
183
+
184
+ echoIfNotSilent(`✅ ${fileName} - transformed`);
185
+
186
+ return Result.ok('transformed');
187
+ }
188
+ } catch (error) {
189
+ const errStr = unknownToString(error);
190
+
191
+ errorIfNotSilent(`❌ ${fileName} - error: ${errStr}`);
192
+
193
+ return Result.err(errStr);
194
+ }
195
+ };
196
+
197
+ await cmd.run(cmdDef, process.argv.slice(2));
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-await-in-loop */
3
+
4
+ import * as cmd from 'cmd-ts';
5
+ import dedent from 'dedent';
6
+ import { castMutable, Result, unknownToString } from 'ts-data-forge';
7
+ import 'ts-repo-utils';
8
+ import {
9
+ replaceRecordWithUnknownRecordTransformer,
10
+ transformSourceCode,
11
+ } from '../functions/index.mjs';
12
+
13
+ const cmdDef = cmd.command({
14
+ name: 'replace-record-with-unknown-record-cli',
15
+ version: '1.0.0',
16
+ args: {
17
+ baseDir: cmd.positional({
18
+ type: cmd.string,
19
+ displayName: 'baseDir',
20
+ description: 'The base directory in which to perform the conversion',
21
+ }),
22
+ exclude: cmd.multioption({
23
+ long: 'exclude',
24
+ type: cmd.optional(cmd.array(cmd.string)),
25
+ description:
26
+ 'Glob patterns of files to exclude from the base directory (e.g., "src/generated/**/*.mts")',
27
+ }),
28
+ silent: cmd.flag({
29
+ long: 'silent',
30
+ type: cmd.optional(cmd.boolean),
31
+ description: 'If true, suppresses output messages (default: false)',
32
+ }),
33
+ },
34
+ handler: (args) => {
35
+ replaceRecordWithUnknownRecordCLI({
36
+ baseDir: args.baseDir,
37
+ exclude: args.exclude ?? [],
38
+ silent: args.silent ?? false,
39
+ }).catch((error: unknown) => {
40
+ console.error('An error occurred:', error);
41
+
42
+ process.exit(1);
43
+ });
44
+ },
45
+ });
46
+
47
+ type Args = Readonly<{
48
+ baseDir: string;
49
+ exclude: readonly string[];
50
+ silent: boolean;
51
+ }>;
52
+
53
+ const replaceRecordWithUnknownRecordCLI = async (
54
+ args: Args,
55
+ ): Promise<Result<undefined, undefined>> => {
56
+ const echoIfNotSilent = args.silent ? () => {} : echo;
57
+
58
+ const errorIfNotSilent = args.silent ? () => {} : console.error;
59
+
60
+ // Find all files matching the glob
61
+ const globResult = await glob(args.baseDir, {
62
+ ignore: castMutable(args.exclude),
63
+ });
64
+
65
+ if (Result.isErr(globResult)) {
66
+ errorIfNotSilent('Error finding files matching pattern:', globResult.value);
67
+
68
+ return Result.err(undefined);
69
+ }
70
+
71
+ const files = globResult.value;
72
+
73
+ if (files.length === 0) {
74
+ echoIfNotSilent('No files found matching pattern:', args.baseDir);
75
+
76
+ return Result.ok(undefined);
77
+ }
78
+
79
+ const { errorFiles, transformedCount, unchangedCount } = await transformFiles(
80
+ files,
81
+ args.silent,
82
+ );
83
+
84
+ const hr = '='.repeat(50);
85
+
86
+ echoIfNotSilent(dedent`
87
+ ${hr}
88
+ Summary:
89
+ ✅ Transformed: ${transformedCount}
90
+ ⏭️ Unchanged: ${unchangedCount}
91
+ ❌ Errors: ${errorFiles.length}
92
+ 📊 Total: ${files.length}
93
+ `);
94
+
95
+ if (errorFiles.length > 0) {
96
+ echoIfNotSilent('\nFiles with errors:');
97
+
98
+ for (const fileName of errorFiles) {
99
+ echoIfNotSilent(` - ${fileName}`);
100
+ }
101
+ }
102
+
103
+ echoIfNotSilent(hr);
104
+
105
+ if (errorFiles.length > 0) {
106
+ return Result.err(undefined);
107
+ }
108
+
109
+ return Result.ok(undefined);
110
+ };
111
+
112
+ const transformFiles = async (
113
+ filePaths: readonly string[],
114
+ silent: boolean,
115
+ ): Promise<
116
+ Readonly<{
117
+ transformedCount: number;
118
+ unchangedCount: number;
119
+ errorFiles: readonly string[];
120
+ }>
121
+ > => {
122
+ let mut_transformedCount: number = 0;
123
+
124
+ let mut_unchangedCount: number = 0;
125
+
126
+ const mut_errorFiles: string[] = [];
127
+
128
+ for (const filePath of filePaths) {
129
+ const result = await transformOneFile(filePath, silent);
130
+
131
+ if (Result.isOk(result)) {
132
+ switch (result.value) {
133
+ case 'transformed':
134
+ mut_transformedCount += 1;
135
+
136
+ break;
137
+
138
+ case 'unchanged':
139
+ mut_unchangedCount += 1;
140
+
141
+ break;
142
+ }
143
+ } else {
144
+ mut_errorFiles.push(path.basename(filePath));
145
+ }
146
+ }
147
+
148
+ return {
149
+ transformedCount: mut_transformedCount,
150
+ unchangedCount: mut_unchangedCount,
151
+ errorFiles: mut_errorFiles,
152
+ };
153
+ };
154
+
155
+ const transformOneFile = async (
156
+ filePath: string,
157
+ silent: boolean,
158
+ ): Promise<Result<'unchanged' | 'transformed', string>> => {
159
+ const echoIfNotSilent = silent ? () => {} : echo;
160
+
161
+ const errorIfNotSilent = silent ? () => {} : console.error;
162
+
163
+ const fileName = path.basename(filePath);
164
+
165
+ const isTsx = fileName.endsWith('.tsx') || fileName.endsWith('.jsx');
166
+
167
+ try {
168
+ const originalCode = await fs.readFile(filePath, 'utf8');
169
+
170
+ // Transform the code with all transformers
171
+ const transformedCode = transformSourceCode(originalCode, isTsx, [
172
+ replaceRecordWithUnknownRecordTransformer(),
173
+ ]);
174
+
175
+ // Check if the code was actually changed
176
+ if (transformedCode === originalCode) {
177
+ echoIfNotSilent(`⏭️ ${fileName} - no changes needed`);
178
+
179
+ return Result.ok('unchanged');
180
+ } else {
181
+ // Write back the transformed code
182
+ await fs.writeFile(filePath, transformedCode, 'utf8');
183
+
184
+ echoIfNotSilent(`✅ ${fileName} - transformed`);
185
+
186
+ return Result.ok('transformed');
187
+ }
188
+ } catch (error) {
189
+ const errStr = unknownToString(error);
190
+
191
+ errorIfNotSilent(`❌ ${fileName} - error: ${errStr}`);
192
+
193
+ return Result.err(errStr);
194
+ }
195
+ };
196
+
197
+ await cmd.run(cmdDef, process.argv.slice(2));
@@ -0,0 +1,154 @@
1
+ import { ISet } from 'ts-data-forge';
2
+ import * as tsm from 'ts-morph';
3
+ import {
4
+ hasDisableNextLineComment,
5
+ isAsConstNode,
6
+ } from '../functions/index.mjs';
7
+ import { type TsMorphTransformer } from './types.mjs';
8
+
9
+ export const appendAsConstTransformer =
10
+ (options?: AppendAsConstTransformerOptions): TsMorphTransformer =>
11
+ (sourceAst) => {
12
+ const ignorePrefixes = ISet.create(options?.ignorePrefixes ?? ['mut_']);
13
+
14
+ const optionsInternal: AppendAsConstTransformerOptionsInternal = {
15
+ applyLevel: options?.applyLevel ?? 'avoidInFunctionArgs',
16
+ ignoredPrefixes: ignorePrefixes,
17
+ };
18
+
19
+ for (const node of sourceAst.getChildren()) {
20
+ transformNode(node, optionsInternal);
21
+ }
22
+ };
23
+
24
+ export type AppendAsConstTransformerOptions = DeepReadonly<{
25
+ applyLevel?: 'all' | 'avoidInFunctionArgs';
26
+
27
+ /**
28
+ * A mute keywords to ignore the readonly conversion.
29
+ *
30
+ * (e.g. `"mut_"`)
31
+ */
32
+ ignorePrefixes?: string[];
33
+
34
+ ignoreConstTypeParameter?: boolean;
35
+ }>;
36
+
37
+ type AppendAsConstTransformerOptionsInternal = DeepReadonly<{
38
+ applyLevel: 'all' | 'avoidInFunctionArgs';
39
+ ignoredPrefixes: ISet<string>;
40
+ }>;
41
+
42
+ const transformNode = (
43
+ node: tsm.Node,
44
+ options: AppendAsConstTransformerOptionsInternal,
45
+ ): void => {
46
+ if (hasDisableNextLineComment(node)) {
47
+ console.debug('skipped by disable-next-line comment');
48
+
49
+ return;
50
+ }
51
+
52
+ // check for ignorePrefix
53
+ if (node.isKind(tsm.SyntaxKind.VariableDeclaration)) {
54
+ const nodeName = node.getName();
55
+
56
+ if (options.ignoredPrefixes.some((p) => nodeName.startsWith(p))) {
57
+ // Skip conversion for variable declarations with ignored prefixes
58
+ // Example: const mut_foo: string[] = []; -> remains as is, without appending `as const`
59
+ console.debug('skipped variable declaration by ignorePrefixes');
60
+
61
+ return;
62
+ }
63
+
64
+ // TODO: Support ignoredPrefixes in ArrayBindingPattern
65
+ // if (ts.isArrayBindingPattern(nodeName)) {
66
+ // // for (const [i, el] of nodeName.elements.entries())
67
+ // }
68
+
69
+ // TODO: Support ignoredPrefixes in ObjectBindingPattern
70
+ // if (ts.isObjectBindingPattern(nodeName)) {
71
+ // // for (const [i, el] of nodeName.elements.entries())
72
+ // }
73
+ }
74
+
75
+ if (
76
+ options.applyLevel === 'avoidInFunctionArgs' &&
77
+ tsm.Node.isCallExpression(node)
78
+ ) {
79
+ return;
80
+ }
81
+
82
+ // `as const` node
83
+ if (isAsConstNode(node)) {
84
+ const expression = removeParenthesis(node.getExpression());
85
+
86
+ if (
87
+ !tsm.Node.isArrayLiteralExpression(expression) &&
88
+ !tsm.Node.isObjectLiteralExpression(expression)
89
+ ) {
90
+ // `as const` is not needed for primitive types
91
+ // Example: `0 as const` -> `0`
92
+ node.replaceWithText(expression.getText());
93
+
94
+ return;
95
+ }
96
+
97
+ // Avoid appending `as const` twice
98
+ removeAsConstRecursively(node.getExpression());
99
+
100
+ return;
101
+ }
102
+
103
+ if (tsm.Node.isArrayLiteralExpression(node)) {
104
+ for (const el of node.getElements()) {
105
+ removeAsConstRecursively(el);
106
+ }
107
+
108
+ node.replaceWithText(`${node.getText()} as const`);
109
+
110
+ return;
111
+ }
112
+
113
+ if (tsm.Node.isObjectLiteralExpression(node)) {
114
+ for (const el of node.getProperties()) {
115
+ removeAsConstRecursively(el);
116
+ }
117
+
118
+ node.replaceWithText(`${node.getText()} as const`);
119
+
120
+ return;
121
+ }
122
+
123
+ for (const child of node.getChildren()) {
124
+ transformNode(child, options);
125
+ }
126
+ };
127
+
128
+ const removeAsConstRecursively = (node: tsm.Node): void => {
129
+ if (hasDisableNextLineComment(node)) {
130
+ console.debug('skipped by disable-next-line comment');
131
+
132
+ return;
133
+ }
134
+
135
+ if (isAsConstNode(node)) {
136
+ // Extract node.expression to remove `as const` and recursively call the function
137
+ // to remove `as const` from nested nodes
138
+ // Example: `[[1,2] as const, [3,4]] as const` -> `[[1,2], [3,4]]`
139
+ removeAsConstRecursively(node.getExpression());
140
+
141
+ node.replaceWithText(node.getExpression().getText());
142
+
143
+ return;
144
+ }
145
+
146
+ for (const child of node.getChildren()) {
147
+ removeAsConstRecursively(child);
148
+ }
149
+ };
150
+
151
+ const removeParenthesis = (node: tsm.Node): tsm.Node =>
152
+ tsm.Node.isParenthesizedExpression(node)
153
+ ? removeParenthesis(node.getExpression())
154
+ : node;