ts-codemod-lib 1.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.
Files changed (149) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +33 -0
  3. package/dist/cmd/convert-to-readonly.d.mts +3 -0
  4. package/dist/cmd/convert-to-readonly.d.mts.map +1 -0
  5. package/dist/cmd/convert-to-readonly.mjs +137 -0
  6. package/dist/cmd/convert-to-readonly.mjs.map +1 -0
  7. package/dist/entry-point.d.mts +2 -0
  8. package/dist/entry-point.d.mts.map +1 -0
  9. package/dist/entry-point.mjs +19 -0
  10. package/dist/entry-point.mjs.map +1 -0
  11. package/dist/functions/ast-transformers/convert-interface-to-type.d.mts +7 -0
  12. package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -0
  13. package/dist/functions/ast-transformers/convert-interface-to-type.mjs +83 -0
  14. package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -0
  15. package/dist/functions/ast-transformers/convert-to-readonly-type.d.mts +29 -0
  16. package/dist/functions/ast-transformers/convert-to-readonly-type.d.mts.map +1 -0
  17. package/dist/functions/ast-transformers/convert-to-readonly-type.mjs +811 -0
  18. package/dist/functions/ast-transformers/convert-to-readonly-type.mjs.map +1 -0
  19. package/dist/functions/ast-transformers/index.d.mts +7 -0
  20. package/dist/functions/ast-transformers/index.d.mts.map +1 -0
  21. package/dist/functions/ast-transformers/index.mjs +9 -0
  22. package/dist/functions/ast-transformers/index.mjs.map +1 -0
  23. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.d.mts +3 -0
  24. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.d.mts.map +1 -0
  25. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs +22 -0
  26. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs.map +1 -0
  27. package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.d.mts +2 -0
  28. package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.d.mts.map +1 -0
  29. package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs +15 -0
  30. package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs.map +1 -0
  31. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts +21 -0
  32. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts.map +1 -0
  33. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +72 -0
  34. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -0
  35. package/dist/functions/ast-transformers/readonly-transformer-helpers/index.d.mts +5 -0
  36. package/dist/functions/ast-transformers/readonly-transformer-helpers/index.d.mts.map +1 -0
  37. package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs +5 -0
  38. package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs.map +1 -0
  39. package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts +37 -0
  40. package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts.map +1 -0
  41. package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs +19 -0
  42. package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs.map +1 -0
  43. package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +7 -0
  44. package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts.map +1 -0
  45. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +173 -0
  46. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -0
  47. package/dist/functions/ast-transformers/transform-source-code.d.mts +3 -0
  48. package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -0
  49. package/dist/functions/ast-transformers/transform-source-code.mjs +27 -0
  50. package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -0
  51. package/dist/functions/ast-transformers/types.d.mts +3 -0
  52. package/dist/functions/ast-transformers/types.d.mts.map +1 -0
  53. package/dist/functions/ast-transformers/types.mjs +2 -0
  54. package/dist/functions/ast-transformers/types.mjs.map +1 -0
  55. package/dist/functions/constants/ignore-comment-text.d.mts +3 -0
  56. package/dist/functions/constants/ignore-comment-text.d.mts.map +1 -0
  57. package/dist/functions/constants/ignore-comment-text.mjs +5 -0
  58. package/dist/functions/constants/ignore-comment-text.mjs.map +1 -0
  59. package/dist/functions/constants/index.d.mts +2 -0
  60. package/dist/functions/constants/index.d.mts.map +1 -0
  61. package/dist/functions/constants/index.mjs +2 -0
  62. package/dist/functions/constants/index.mjs.map +1 -0
  63. package/dist/functions/functions/has-disable-next-line-comment.d.mts +10 -0
  64. package/dist/functions/functions/has-disable-next-line-comment.d.mts.map +1 -0
  65. package/dist/functions/functions/has-disable-next-line-comment.mjs +47 -0
  66. package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -0
  67. package/dist/functions/functions/index.d.mts +9 -0
  68. package/dist/functions/functions/index.d.mts.map +1 -0
  69. package/dist/functions/functions/index.mjs +9 -0
  70. package/dist/functions/functions/index.mjs.map +1 -0
  71. package/dist/functions/functions/is-as-const-node.d.mts +10 -0
  72. package/dist/functions/functions/is-as-const-node.d.mts.map +1 -0
  73. package/dist/functions/functions/is-as-const-node.mjs +30 -0
  74. package/dist/functions/functions/is-as-const-node.mjs.map +1 -0
  75. package/dist/functions/functions/is-primitive-type-node.d.mts +15 -0
  76. package/dist/functions/functions/is-primitive-type-node.d.mts.map +1 -0
  77. package/dist/functions/functions/is-primitive-type-node.mjs +46 -0
  78. package/dist/functions/functions/is-primitive-type-node.mjs.map +1 -0
  79. package/dist/functions/functions/is-readonly-node.d.mts +21 -0
  80. package/dist/functions/functions/is-readonly-node.d.mts.map +1 -0
  81. package/dist/functions/functions/is-readonly-node.mjs +30 -0
  82. package/dist/functions/functions/is-readonly-node.mjs.map +1 -0
  83. package/dist/functions/functions/is-spread-parameter-node.d.mts +4 -0
  84. package/dist/functions/functions/is-spread-parameter-node.d.mts.map +1 -0
  85. package/dist/functions/functions/is-spread-parameter-node.mjs +9 -0
  86. package/dist/functions/functions/is-spread-parameter-node.mjs.map +1 -0
  87. package/dist/functions/functions/remove-parentheses.d.mts +3 -0
  88. package/dist/functions/functions/remove-parentheses.d.mts.map +1 -0
  89. package/dist/functions/functions/remove-parentheses.mjs +9 -0
  90. package/dist/functions/functions/remove-parentheses.mjs.map +1 -0
  91. package/dist/functions/functions/unwrap-readonly.d.mts +3 -0
  92. package/dist/functions/functions/unwrap-readonly.d.mts.map +1 -0
  93. package/dist/functions/functions/unwrap-readonly.mjs +8 -0
  94. package/dist/functions/functions/unwrap-readonly.mjs.map +1 -0
  95. package/dist/functions/functions/wrap-with-parentheses.d.mts +2 -0
  96. package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -0
  97. package/dist/functions/functions/wrap-with-parentheses.mjs +4 -0
  98. package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -0
  99. package/dist/functions/index.d.mts +5 -0
  100. package/dist/functions/index.d.mts.map +1 -0
  101. package/dist/functions/index.mjs +19 -0
  102. package/dist/functions/index.mjs.map +1 -0
  103. package/dist/functions/utils/index.d.mts +2 -0
  104. package/dist/functions/utils/index.d.mts.map +1 -0
  105. package/dist/functions/utils/index.mjs +2 -0
  106. package/dist/functions/utils/index.mjs.map +1 -0
  107. package/dist/functions/utils/replace-with-debug.d.mts +3 -0
  108. package/dist/functions/utils/replace-with-debug.d.mts.map +1 -0
  109. package/dist/functions/utils/replace-with-debug.mjs +7 -0
  110. package/dist/functions/utils/replace-with-debug.mjs.map +1 -0
  111. package/dist/globals.d.mts +1 -0
  112. package/dist/index.d.mts +2 -0
  113. package/dist/index.d.mts.map +1 -0
  114. package/dist/index.mjs +19 -0
  115. package/dist/index.mjs.map +1 -0
  116. package/dist/tsconfig.json +1 -0
  117. package/dist/types.d.mts +2 -0
  118. package/package.json +134 -0
  119. package/src/cmd/convert-to-readonly.mts +195 -0
  120. package/src/entry-point.mts +1 -0
  121. package/src/functions/ast-transformers/convert-interface-to-type.mts +119 -0
  122. package/src/functions/ast-transformers/convert-interface-to-type.test.mts +295 -0
  123. package/src/functions/ast-transformers/convert-to-readonly-type.mts +1391 -0
  124. package/src/functions/ast-transformers/convert-to-readonly-type.test.mts +3653 -0
  125. package/src/functions/ast-transformers/index.mts +6 -0
  126. package/src/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mts +24 -0
  127. package/src/functions/ast-transformers/readonly-transformer-helpers/constants.mts +12 -0
  128. package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +152 -0
  129. package/src/functions/ast-transformers/readonly-transformer-helpers/index.mts +4 -0
  130. package/src/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mts +65 -0
  131. package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +238 -0
  132. package/src/functions/ast-transformers/transform-source-code.mts +38 -0
  133. package/src/functions/ast-transformers/types.mts +6 -0
  134. package/src/functions/constants/ignore-comment-text.mts +3 -0
  135. package/src/functions/constants/index.mts +1 -0
  136. package/src/functions/functions/has-disable-next-line-comment.mts +56 -0
  137. package/src/functions/functions/index.mts +8 -0
  138. package/src/functions/functions/is-as-const-node.mts +47 -0
  139. package/src/functions/functions/is-primitive-type-node.mts +301 -0
  140. package/src/functions/functions/is-readonly-node.mts +247 -0
  141. package/src/functions/functions/is-spread-parameter-node.mts +13 -0
  142. package/src/functions/functions/remove-parentheses.mts +7 -0
  143. package/src/functions/functions/unwrap-readonly.mts +7 -0
  144. package/src/functions/functions/wrap-with-parentheses.mts +2 -0
  145. package/src/functions/index.mts +4 -0
  146. package/src/functions/utils/index.mts +1 -0
  147. package/src/functions/utils/replace-with-debug.mts +10 -0
  148. package/src/globals.d.mts +1 -0
  149. package/src/index.mts +1 -0
@@ -0,0 +1,301 @@
1
+ import { ISet } from 'ts-data-forge';
2
+ import * as tsm from 'ts-morph';
3
+
4
+ // Define the set of SyntaxKinds that represent primitive type keywords
5
+ const primitiveKeywordKinds = ISet.create<tsm.SyntaxKind>([
6
+ tsm.SyntaxKind.StringKeyword,
7
+ tsm.SyntaxKind.BooleanKeyword,
8
+ tsm.SyntaxKind.NumberKeyword,
9
+ tsm.SyntaxKind.BigIntKeyword,
10
+ tsm.SyntaxKind.SymbolKeyword,
11
+ tsm.SyntaxKind.UndefinedKeyword,
12
+ tsm.SyntaxKind.VoidKeyword,
13
+ tsm.SyntaxKind.AnyKeyword,
14
+ tsm.SyntaxKind.UnknownKeyword,
15
+ tsm.SyntaxKind.ObjectKeyword, // Note: 'object' is sometimes considered primitive in TS type system context
16
+ tsm.SyntaxKind.NeverKeyword,
17
+ ]);
18
+
19
+ export type PrimitiveTypeNode = tsm.Node &
20
+ Readonly<
21
+ | tsm.LiteralTypeNode
22
+ | tsm.TemplateLiteralTypeNode
23
+ | (tsm.TypeNode & {
24
+ kind:
25
+ | tsm.SyntaxKind.StringKeyword
26
+ | tsm.SyntaxKind.BooleanKeyword
27
+ | tsm.SyntaxKind.NumberKeyword
28
+ | tsm.SyntaxKind.BigIntKeyword
29
+ | tsm.SyntaxKind.SymbolKeyword
30
+ | tsm.SyntaxKind.UndefinedKeyword
31
+ | tsm.SyntaxKind.VoidKeyword
32
+ | tsm.SyntaxKind.AnyKeyword
33
+ | tsm.SyntaxKind.UnknownKeyword
34
+ | tsm.SyntaxKind.ObjectKeyword
35
+ | tsm.SyntaxKind.NeverKeyword;
36
+ })
37
+ >;
38
+
39
+ /**
40
+ * Checks if a given ts-morph node represents a primitive type node.
41
+ * This includes keyword types (string, number, etc.), literal types (null, "abc", 123),
42
+ * and template literal types.
43
+ *
44
+ * @param node - The ts-morph node to check.
45
+ * @returns True if the node represents a primitive type node, false otherwise.
46
+ * Acts as a type guard.
47
+ */
48
+ export const isPrimitiveTypeNode = (
49
+ node: tsm.Node,
50
+ ): node is PrimitiveTypeNode => {
51
+ // Check for literal types (null, "aaa", 1.23, 456n, true, false)
52
+ if (node.isKind(tsm.SyntaxKind.LiteralType)) {
53
+ return true;
54
+ }
55
+
56
+ // Check for template literal types (`abc${expr}def`)
57
+ if (node.isKind(tsm.SyntaxKind.TemplateLiteralType)) {
58
+ return true;
59
+ }
60
+
61
+ // Check if it's a TypeNode and its kind is one of the primitive keywords
62
+ // Node.isTypeNode(node) ensures we only check nodes that represent types
63
+ // if (ts.Node.isTypeNode(node) && primitiveKeywordKinds.has(node.getKind())) {
64
+ if (primitiveKeywordKinds.has(node.getKind())) {
65
+ return true;
66
+ }
67
+
68
+ return false;
69
+ };
70
+
71
+ if (import.meta.vitest !== undefined) {
72
+ // Helper to get a specific type node from source code
73
+ const getTypeNodeFromSource = (
74
+ sourceCode: string,
75
+ typeName: string,
76
+ ): tsm.TypeNode => {
77
+ const project = new tsm.Project({ useInMemoryFileSystem: true });
78
+
79
+ const sourceFile = project.createSourceFile('test.ts', sourceCode);
80
+
81
+ const typeAlias = sourceFile.getTypeAliasOrThrow(typeName);
82
+
83
+ return typeAlias.getTypeNodeOrThrow();
84
+ };
85
+
86
+ // Helper to get the first node of a specific kind from source code
87
+ const getFirstNodeOfKind = <T extends tsm.Node>(
88
+ sourceCode: string,
89
+ kind: tsm.SyntaxKind,
90
+ ): T | undefined => {
91
+ const project = new tsm.Project({ useInMemoryFileSystem: true });
92
+
93
+ const sourceFile = project.createSourceFile('test.ts', sourceCode);
94
+
95
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
96
+ return sourceFile.getFirstDescendantByKind(kind) as T | undefined;
97
+ };
98
+
99
+ describe('isPrimitiveTypeNode', () => {
100
+ describe('positive cases', () => {
101
+ test.each([
102
+ {
103
+ name: 'string',
104
+ code: 'type Test = string;',
105
+ kind: tsm.SyntaxKind.StringKeyword,
106
+ },
107
+ {
108
+ name: 'number',
109
+ code: 'type Test = number;',
110
+ kind: tsm.SyntaxKind.NumberKeyword,
111
+ },
112
+ {
113
+ name: 'boolean',
114
+ code: 'type Test = boolean;',
115
+ kind: tsm.SyntaxKind.BooleanKeyword,
116
+ },
117
+ {
118
+ name: 'bigint',
119
+ code: 'type Test = bigint;',
120
+ kind: tsm.SyntaxKind.BigIntKeyword,
121
+ },
122
+ {
123
+ name: 'symbol',
124
+ code: 'type Test = symbol;',
125
+ kind: tsm.SyntaxKind.SymbolKeyword,
126
+ },
127
+ {
128
+ name: 'undefined',
129
+ code: 'type Test = undefined;',
130
+ kind: tsm.SyntaxKind.UndefinedKeyword,
131
+ },
132
+ {
133
+ name: 'void',
134
+ code: 'type Test = void;',
135
+ kind: tsm.SyntaxKind.VoidKeyword,
136
+ },
137
+ {
138
+ name: 'any',
139
+ code: 'type Test = any;',
140
+ kind: tsm.SyntaxKind.AnyKeyword,
141
+ },
142
+ {
143
+ name: 'unknown',
144
+ code: 'type Test = unknown;',
145
+ kind: tsm.SyntaxKind.UnknownKeyword,
146
+ },
147
+ {
148
+ name: 'never',
149
+ code: 'type Test = never;',
150
+ kind: tsm.SyntaxKind.NeverKeyword,
151
+ },
152
+ {
153
+ name: 'object',
154
+ code: 'type Test = object;',
155
+ kind: tsm.SyntaxKind.ObjectKeyword,
156
+ }, // Included based on original code
157
+ {
158
+ name: 'null',
159
+ code: 'type Test = null;',
160
+ kind: tsm.SyntaxKind.LiteralType,
161
+ },
162
+ {
163
+ name: 'string literal ("hello")',
164
+ code: 'type Test = "hello";',
165
+ kind: tsm.SyntaxKind.LiteralType,
166
+ },
167
+ {
168
+ name: 'number literal (123)',
169
+ code: 'type Test = 123;',
170
+ kind: tsm.SyntaxKind.LiteralType,
171
+ },
172
+ {
173
+ name: 'bigint literal (123n)',
174
+ code: 'type Test = 123n;',
175
+ kind: tsm.SyntaxKind.LiteralType,
176
+ },
177
+ {
178
+ name: 'boolean literal (true)',
179
+ code: 'type Test = true;',
180
+ kind: tsm.SyntaxKind.LiteralType,
181
+ },
182
+ {
183
+ name: 'boolean literal (false)',
184
+ code: 'type Test = false;',
185
+ kind: tsm.SyntaxKind.LiteralType,
186
+ },
187
+ {
188
+ name: 'TemplateLiteral',
189
+ // eslint-disable-next-line no-template-curly-in-string
190
+ code: 'type Test = `a${string}b`;',
191
+ kind: tsm.SyntaxKind.TemplateLiteralType,
192
+ },
193
+ ])('$name', ({ code, kind }) => {
194
+ const node = getTypeNodeFromSource(code, 'Test');
195
+
196
+ expect(node.getKind()).toBe(kind);
197
+
198
+ assert.isTrue(isPrimitiveTypeNode(node));
199
+ });
200
+ });
201
+
202
+ describe('negative cases', () => {
203
+ test.each([
204
+ {
205
+ name: 'Date TypeReference',
206
+ code: 'type Test = Date;',
207
+ kind: tsm.SyntaxKind.TypeReference,
208
+ },
209
+ {
210
+ name: 'Array Type',
211
+ code: 'type Test = number[];',
212
+ kind: tsm.SyntaxKind.ArrayType,
213
+ },
214
+ {
215
+ name: 'Tuple Type',
216
+ code: 'type Test = [string, number];',
217
+ kind: tsm.SyntaxKind.TupleType,
218
+ },
219
+ {
220
+ name: 'Type Literal',
221
+ code: 'type Test = { a: number };',
222
+ kind: tsm.SyntaxKind.TypeLiteral,
223
+ },
224
+ {
225
+ name: 'Readonly Type Operator',
226
+ code: 'type Test = readonly string[];',
227
+ kind: tsm.SyntaxKind.TypeOperator,
228
+ },
229
+ {
230
+ name: 'Union Type',
231
+ code: 'type Test = string | number;',
232
+ kind: tsm.SyntaxKind.UnionType,
233
+ },
234
+ {
235
+ name: 'Intersection Type',
236
+ code: 'type Test = A & B;',
237
+ kind: tsm.SyntaxKind.IntersectionType,
238
+ },
239
+ {
240
+ name: 'Mapped Type',
241
+ code: 'type Test = { [K in keyof T]: T[K] };',
242
+ kind: tsm.SyntaxKind.MappedType,
243
+ }, // Needs T defined
244
+ {
245
+ name: 'Parenthesized Type',
246
+ code: 'type Test = (string);',
247
+ kind: tsm.SyntaxKind.ParenthesizedType,
248
+ },
249
+ {
250
+ name: 'Conditional Type',
251
+ code: 'type Test = T extends U ? X : Y;',
252
+ kind: tsm.SyntaxKind.ConditionalType,
253
+ }, // Needs T, U, X, Y
254
+ {
255
+ name: 'Indexed Access Type',
256
+ code: 'type Test = T[K];',
257
+ kind: tsm.SyntaxKind.IndexedAccessType,
258
+ }, // Needs T, K
259
+ {
260
+ name: 'Type Query (typeof)',
261
+ code: 'type Test = typeof myVar;',
262
+ kind: tsm.SyntaxKind.TypeQuery,
263
+ },
264
+ ])('$name', ({ code, kind }) => {
265
+ const node = getTypeNodeFromSource(code, 'Test');
266
+
267
+ expect(node.getKind()).toBe(kind); // Verify node type
268
+
269
+ assert.isFalse(isPrimitiveTypeNode(node));
270
+ });
271
+
272
+ test.each([
273
+ {
274
+ name: 'Type Predicate',
275
+ code: 'function isString(x: any): x is string { return typeof x === "string"; }',
276
+ kind: tsm.SyntaxKind.TypePredicate,
277
+ }, // Node is part of function sig
278
+ {
279
+ name: 'This Type',
280
+ code: 'class C { method(): this {} }',
281
+ kind: tsm.SyntaxKind.ThisType,
282
+ }, // Node is part of method sig
283
+ {
284
+ name: 'Infer Type',
285
+ code: 'type Test<T> = T extends Promise<infer R> ? R : T;',
286
+ kind: tsm.SyntaxKind.InferType,
287
+ }, // Needs context
288
+ ])('$name (not a type node)', ({ code, kind }) => {
289
+ const node = getFirstNodeOfKind(code, kind);
290
+
291
+ expect(node).toBeDefined();
292
+
293
+ if (node === undefined) {
294
+ throw new Error('Node should be defined');
295
+ }
296
+
297
+ assert.isFalse(isPrimitiveTypeNode(node));
298
+ });
299
+ });
300
+ });
301
+ }
@@ -0,0 +1,247 @@
1
+ import { Arr, expectType } from 'ts-data-forge';
2
+ import * as tsm from 'ts-morph';
3
+ import { isPrimitiveTypeNode } from './is-primitive-type-node.mjs';
4
+
5
+ export const isShallowReadonlyTypeNode = (node: tsm.Node): boolean =>
6
+ isReadonlyTupleOrArrayTypeNode(node) ||
7
+ isReadonlyTypeReferenceNode(node) ||
8
+ isPrimitiveTypeNode(node);
9
+
10
+ export const isReadonlyTupleOrArrayTypeNode = (
11
+ node: tsm.Node,
12
+ ): node is ReadonlyArrayTypeNode | ReadonlyTupleTypeNode =>
13
+ node.isKind(tsm.SyntaxKind.TypeOperator) &&
14
+ node.getOperator() === tsm.SyntaxKind.ReadonlyKeyword &&
15
+ (node.getTypeNode().isKind(tsm.SyntaxKind.ArrayType) || // Use optional chaining and isKind
16
+ node.getTypeNode().isKind(tsm.SyntaxKind.TupleType));
17
+
18
+ export type ReadonlyArrayTypeNode = tsm.TypeNode &
19
+ Omit<tsm.TypeOperatorTypeNode, 'getOperator' | 'getTypeNode'> &
20
+ Readonly<{
21
+ getOperator: () => tsm.SyntaxKind.ReadonlyKeyword;
22
+ getTypeNode: () => tsm.ArrayTypeNode;
23
+ }>;
24
+
25
+ export const isReadonlyArrayTypeNode = (
26
+ node: tsm.Node,
27
+ ): node is ReadonlyArrayTypeNode =>
28
+ node.isKind(tsm.SyntaxKind.TypeOperator) &&
29
+ node.getOperator() === tsm.SyntaxKind.ReadonlyKeyword &&
30
+ node.getTypeNode().isKind(tsm.SyntaxKind.ArrayType);
31
+
32
+ // Helper to get a specific type node from source code
33
+ const getTypeNodeFromSource = (
34
+ sourceCode: string,
35
+ typeName: string,
36
+ ): tsm.Node => {
37
+ const project = new tsm.Project({ useInMemoryFileSystem: true });
38
+
39
+ const sourceFile = project.createSourceFile('test.ts', sourceCode);
40
+
41
+ const typeAlias = sourceFile.getTypeAliasOrThrow(typeName);
42
+
43
+ return typeAlias.getTypeNodeOrThrow();
44
+ };
45
+
46
+ if (import.meta.vitest !== undefined) {
47
+ describe('isReadonlyArrayTypeNode', () => {
48
+ test('should return true for readonly array type', () => {
49
+ const node = getTypeNodeFromSource(
50
+ 'type Test = readonly number[];',
51
+ 'Test',
52
+ );
53
+
54
+ if (isReadonlyArrayTypeNode(node)) {
55
+ const _operator = node.getOperator();
56
+
57
+ expectType<typeof _operator, tsm.SyntaxKind.ReadonlyKeyword>('=');
58
+
59
+ const _typeNode = node.getTypeNode();
60
+
61
+ expectType<typeof _typeNode, tsm.ArrayTypeNode>('=');
62
+ }
63
+
64
+ assert.isTrue(node.isKind(tsm.SyntaxKind.TypeOperator));
65
+
66
+ if (!isReadonlyArrayTypeNode(node)) {
67
+ throw new Error('node should be ReadonlyArrayTypeNode');
68
+ }
69
+
70
+ expect(node.getOperator()).toBe(tsm.SyntaxKind.ReadonlyKeyword);
71
+
72
+ assert.isTrue(node.getTypeNode().isKind(tsm.SyntaxKind.ArrayType));
73
+ });
74
+
75
+ test('should return false for non-readonly array', () => {
76
+ const node = getTypeNodeFromSource('type Test = string[];', 'Test');
77
+
78
+ assert.isFalse(isReadonlyArrayTypeNode(node));
79
+ });
80
+
81
+ test('should return false for readonly tuple', () => {
82
+ const node = getTypeNodeFromSource(
83
+ 'type Test = readonly [string];',
84
+ 'Test',
85
+ );
86
+
87
+ assert.isFalse(isReadonlyArrayTypeNode(node));
88
+ });
89
+ });
90
+ }
91
+
92
+ export type ReadonlyTupleTypeNode = tsm.TypeNode &
93
+ Omit<tsm.TypeOperatorTypeNode, 'getOperator' | 'getTypeNode'> &
94
+ Readonly<{
95
+ getOperator: () => tsm.SyntaxKind.ReadonlyKeyword;
96
+ getTypeNode: () => tsm.TupleTypeNode;
97
+ }>;
98
+
99
+ export const isReadonlyTupleTypeNode = (
100
+ node: tsm.Node,
101
+ ): node is ReadonlyTupleTypeNode =>
102
+ node.isKind(tsm.SyntaxKind.TypeOperator) &&
103
+ node.getOperator() === tsm.SyntaxKind.ReadonlyKeyword &&
104
+ node.getTypeNode().isKind(tsm.SyntaxKind.TupleType);
105
+
106
+ if (import.meta.vitest !== undefined) {
107
+ describe('isReadonlyTupleTypeNode', () => {
108
+ test('should return true for readonly tuple type', () => {
109
+ const node = getTypeNodeFromSource(
110
+ 'type Test = readonly [number, string];',
111
+ 'Test',
112
+ );
113
+
114
+ assert.isTrue(isReadonlyTupleTypeNode(node));
115
+
116
+ if (isReadonlyTupleTypeNode(node)) {
117
+ const _operator = node.getOperator();
118
+
119
+ expectType<typeof _operator, tsm.SyntaxKind.ReadonlyKeyword>('=');
120
+
121
+ const _typeNode = node.getTypeNode();
122
+
123
+ expectType<typeof _typeNode, tsm.TupleTypeNode>('=');
124
+ }
125
+
126
+ assert.isTrue(node.isKind(tsm.SyntaxKind.TypeOperator));
127
+
128
+ if (!isReadonlyTupleTypeNode(node)) {
129
+ throw new Error('node should be ReadonlyTupleTypeNode');
130
+ }
131
+
132
+ expect(node.getOperator()).toBe(tsm.SyntaxKind.ReadonlyKeyword);
133
+
134
+ assert.isTrue(node.getTypeNode().isKind(tsm.SyntaxKind.TupleType));
135
+ });
136
+
137
+ test('should return false for non-readonly tuple', () => {
138
+ const node = getTypeNodeFromSource('type Test = [boolean];', 'Test');
139
+
140
+ assert.isFalse(isReadonlyTupleTypeNode(node));
141
+ });
142
+
143
+ test('should return false for readonly array', () => {
144
+ const node = getTypeNodeFromSource(
145
+ 'type Test = readonly number[];',
146
+ 'Test',
147
+ );
148
+
149
+ assert.isFalse(isReadonlyTupleTypeNode(node));
150
+ });
151
+ });
152
+ }
153
+
154
+ export type ReadonlyTypeReferenceNode = tsm.TypeNode &
155
+ Omit<tsm.TypeReferenceNode, 'getTypeName' | 'getTypeArguments'> &
156
+ Readonly<{
157
+ getTypeName: () => Omit<tsm.Identifier, 'getText'> &
158
+ Readonly<{
159
+ getText: () => 'Readonly';
160
+ }>;
161
+ getTypeArguments: () => readonly [tsm.TypeNode];
162
+ }>;
163
+
164
+ export const isReadonlyTypeReferenceNode = (
165
+ node: tsm.Node,
166
+ ): node is ReadonlyTypeReferenceNode => {
167
+ if (!node.isKind(tsm.SyntaxKind.TypeReference)) {
168
+ return false;
169
+ }
170
+
171
+ const typeName = node.getTypeName();
172
+
173
+ const typeArguments = node.getTypeArguments();
174
+
175
+ return (
176
+ typeName.isKind(tsm.SyntaxKind.Identifier) &&
177
+ typeName.getText() === 'Readonly' &&
178
+ Arr.isArrayOfLength(typeArguments, 1)
179
+ );
180
+ };
181
+
182
+ if (import.meta.vitest !== undefined) {
183
+ describe('isReadonlyTypeReferenceNode', () => {
184
+ test('should return true for Readonly<T>', () => {
185
+ const node = getTypeNodeFromSource(
186
+ 'type Test = Readonly<{ x: number }>;',
187
+ 'Test',
188
+ );
189
+
190
+ assert.isTrue(isReadonlyTypeReferenceNode(node));
191
+
192
+ if (isReadonlyTypeReferenceNode(node)) {
193
+ const _typeNameText = node.getTypeName().getText();
194
+
195
+ expectType<typeof _typeNameText, 'Readonly'>('=');
196
+
197
+ const _typeArguments = node.getTypeArguments();
198
+
199
+ expectType<
200
+ typeof _typeArguments,
201
+ readonly [tsm.TypeNode<tsm.ts.TypeNode>]
202
+ >('=');
203
+ }
204
+
205
+ assert.isTrue(node.isKind(tsm.SyntaxKind.TypeReference));
206
+
207
+ if (!isReadonlyTypeReferenceNode(node)) {
208
+ throw new Error('node should be ReadonlyNode');
209
+ }
210
+
211
+ expect(node.getTypeName().getText()).toBe('Readonly');
212
+
213
+ expect(node.getTypeArguments()).toHaveLength(1);
214
+ });
215
+
216
+ test('should return false for other type references', () => {
217
+ const node = getTypeNodeFromSource(
218
+ 'type Test = Partial<{ y: string }>;',
219
+ 'Test',
220
+ );
221
+
222
+ assert.isFalse(isReadonlyTypeReferenceNode(node));
223
+ });
224
+
225
+ test('should return false for Readonly without type arguments', () => {
226
+ // Note: This is syntactically incorrect TS, but testing the guard
227
+ const node = getTypeNodeFromSource('type Test = Readonly;', 'Test');
228
+
229
+ assert.isFalse(isReadonlyTypeReferenceNode(node));
230
+ });
231
+
232
+ test('should return false for Readonly with multiple type arguments', () => {
233
+ // Note: This is syntactically incorrect TS, but testing the guard
234
+ const project = new tsm.Project({ useInMemoryFileSystem: true });
235
+
236
+ // Need to create manually as TS parser might reject Readonly<A, B>
237
+ const sourceFile = project.createSourceFile(
238
+ 'test.ts',
239
+ 'type Test = Readonly<string, number>;',
240
+ );
241
+
242
+ const node = sourceFile.getTypeAliasOrThrow('Test').getTypeNodeOrThrow();
243
+
244
+ assert.isFalse(isReadonlyTypeReferenceNode(node));
245
+ });
246
+ });
247
+ }
@@ -0,0 +1,13 @@
1
+ import * as tsm from 'ts-morph';
2
+
3
+ export const isSpreadParameterNode = (
4
+ node: tsm.Node,
5
+ ): node is tsm.ParameterDeclaration =>
6
+ node.isKind(tsm.SyntaxKind.Parameter) &&
7
+ node.getDotDotDotToken() !== undefined;
8
+
9
+ export const isSpreadNamedTupleMemberNode = (
10
+ node: tsm.Node,
11
+ ): node is tsm.NamedTupleMember =>
12
+ node.isKind(tsm.SyntaxKind.NamedTupleMember) &&
13
+ node.getDotDotDotToken() !== undefined;
@@ -0,0 +1,7 @@
1
+ import * as tsm from 'ts-morph';
2
+
3
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
4
+ export const removeParentheses = (node: tsm.TypeNode): tsm.TypeNode =>
5
+ node.isKind(tsm.SyntaxKind.ParenthesizedType)
6
+ ? removeParentheses(node.getTypeNode())
7
+ : node;
@@ -0,0 +1,7 @@
1
+ import { type ReadonlyTypeReferenceNode } from './is-readonly-node.mjs';
2
+ import { wrapWithParentheses } from './wrap-with-parentheses.mjs';
3
+
4
+ export const unwrapReadonlyTypeArgText = (
5
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
6
+ node: ReadonlyTypeReferenceNode,
7
+ ): string => wrapWithParentheses(node.getTypeArguments()[0].getFullText());
@@ -0,0 +1,2 @@
1
+ export const wrapWithParentheses = (nodeStr: string): string =>
2
+ `(${nodeStr.trim()})`;
@@ -0,0 +1,4 @@
1
+ export * from './ast-transformers/index.mjs';
2
+ export * from './constants/index.mjs';
3
+ export * from './functions/index.mjs';
4
+ export * from './utils/index.mjs';
@@ -0,0 +1 @@
1
+ export * from './replace-with-debug.mjs';
@@ -0,0 +1,10 @@
1
+ import type * as tsm from 'ts-morph';
2
+
3
+ export const replaceNodeWithDebugPrint = (
4
+ node: tsm.Node,
5
+ newNodeText: string,
6
+ ): void => {
7
+ console.debug(`${node.getText()} -> ${newNodeText}`);
8
+
9
+ node.replaceWithText(newNodeText);
10
+ };
@@ -0,0 +1 @@
1
+ /// <reference types="ts-type-forge" />
package/src/index.mts ADDED
@@ -0,0 +1 @@
1
+ export * from './functions/index.mjs';