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,6 @@
1
+ export * from './convert-interface-to-type.mjs';
2
+ export * from './convert-to-readonly-type.mjs';
3
+ export * from './readonly-transformer-helpers/index.mjs';
4
+ export * from './replace-record-with-unknown-record.mjs';
5
+ export * from './transform-source-code.mjs';
6
+ export * from './types.mjs';
@@ -0,0 +1,24 @@
1
+ import * as tsm from 'ts-morph';
2
+ import {
3
+ isPrimitiveTypeNode,
4
+ isReadonlyTupleOrArrayTypeNode,
5
+ } from '../../functions/index.mjs';
6
+
7
+ export const compareUnionIntersectionTypes = (
8
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
9
+ a: tsm.TypeNode,
10
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
11
+ b: tsm.TypeNode,
12
+ ): number => mapRank(a) - mapRank(b);
13
+
14
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
15
+ const mapRank = (t: tsm.TypeNode): number =>
16
+ isPrimitiveTypeNode(t)
17
+ ? 0
18
+ : t.isKind(tsm.SyntaxKind.ArrayType) ||
19
+ t.isKind(tsm.SyntaxKind.TupleType) ||
20
+ isReadonlyTupleOrArrayTypeNode(t)
21
+ ? 1
22
+ : t.isKind(tsm.SyntaxKind.TypeLiteral)
23
+ ? 2
24
+ : 3;
@@ -0,0 +1,12 @@
1
+ import { ISet } from 'ts-data-forge';
2
+
3
+ export const invalidDeepReadonlyTypeName = ISet.create([
4
+ 'Readonly',
5
+ 'readonly',
6
+ 'ReadonlyArray',
7
+ 'Array',
8
+ 'Set',
9
+ 'Map',
10
+ 'ReadonlySet',
11
+ 'ReadonlyMap',
12
+ ]);
@@ -0,0 +1,152 @@
1
+ import * as tsm from 'ts-morph';
2
+ import {
3
+ isPrimitiveTypeNode,
4
+ isReadonlyTupleOrArrayTypeNode,
5
+ isReadonlyTypeReferenceNode,
6
+ removeParentheses,
7
+ type PrimitiveTypeNode,
8
+ type ReadonlyArrayTypeNode,
9
+ type ReadonlyTupleTypeNode,
10
+ type ReadonlyTypeReferenceNode,
11
+ } from '../../functions/index.mjs';
12
+
13
+ export const groupUnionIntersectionTypes = (
14
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
15
+ types: readonly tsm.TypeNode[],
16
+ ): Readonly<{
17
+ primitives:
18
+ | Readonly<{
19
+ firstPosition: number;
20
+ nodes: readonly PrimitiveTypeNode[];
21
+ }>
22
+ | undefined;
23
+
24
+ arraysAndTuples:
25
+ | Readonly<{
26
+ firstPosition: number;
27
+ nodes: readonly (
28
+ | tsm.ArrayTypeNode
29
+ | tsm.TupleTypeNode
30
+ | ReadonlyArrayTypeNode
31
+ | ReadonlyTupleTypeNode
32
+ )[];
33
+ }>
34
+ | undefined;
35
+
36
+ typeLiterals:
37
+ | Readonly<{
38
+ firstPosition: number;
39
+ nodes: readonly (tsm.TypeLiteralNode | ReadonlyTypeReferenceNode)[];
40
+ }>
41
+ | undefined;
42
+
43
+ others:
44
+ | Readonly<{
45
+ firstPosition: number;
46
+ nodes: readonly tsm.TypeNode[];
47
+ }>
48
+ | undefined;
49
+ }> => {
50
+ const mut_grouped: {
51
+ primitives:
52
+ | Readonly<{
53
+ firstPosition: number;
54
+ nodes: PrimitiveTypeNode[];
55
+ }>
56
+ | undefined;
57
+
58
+ arraysAndTuples:
59
+ | Readonly<{
60
+ firstPosition: number;
61
+ nodes: (
62
+ | tsm.ArrayTypeNode
63
+ | tsm.TupleTypeNode
64
+ | ReadonlyArrayTypeNode
65
+ | ReadonlyTupleTypeNode
66
+ )[];
67
+ }>
68
+ | undefined;
69
+
70
+ typeLiterals:
71
+ | Readonly<{
72
+ firstPosition: number;
73
+ nodes: (tsm.TypeLiteralNode | ReadonlyTypeReferenceNode)[];
74
+ }>
75
+ | undefined;
76
+
77
+ others:
78
+ | Readonly<{
79
+ firstPosition: number;
80
+ nodes: tsm.TypeNode[];
81
+ }>
82
+ | undefined;
83
+ } = {
84
+ primitives: undefined,
85
+ arraysAndTuples: undefined,
86
+ typeLiterals: undefined,
87
+ others: undefined,
88
+ };
89
+
90
+ for (const [i, t_] of types.entries()) {
91
+ const t = removeParentheses(t_);
92
+
93
+ // isReadonlyTypeReferenceNode
94
+ if (isPrimitiveTypeNode(t)) {
95
+ if (mut_grouped.primitives === undefined) {
96
+ mut_grouped.primitives = {
97
+ firstPosition: i,
98
+ nodes: [t],
99
+ } as const;
100
+ } else {
101
+ mut_grouped.primitives.nodes.push(t);
102
+ }
103
+
104
+ continue;
105
+ }
106
+
107
+ if (
108
+ t.isKind(tsm.SyntaxKind.ArrayType) ||
109
+ t.isKind(tsm.SyntaxKind.TupleType) ||
110
+ isReadonlyTupleOrArrayTypeNode(t)
111
+ ) {
112
+ if (mut_grouped.arraysAndTuples === undefined) {
113
+ mut_grouped.arraysAndTuples = {
114
+ firstPosition: i,
115
+ nodes: [t],
116
+ } as const;
117
+ } else {
118
+ mut_grouped.arraysAndTuples.nodes.push(t);
119
+ }
120
+
121
+ continue;
122
+ }
123
+
124
+ if (
125
+ t.isKind(tsm.SyntaxKind.TypeLiteral) ||
126
+ isReadonlyTypeReferenceNode(t)
127
+ ) {
128
+ if (mut_grouped.typeLiterals === undefined) {
129
+ mut_grouped.typeLiterals = {
130
+ firstPosition: i,
131
+ nodes: [t],
132
+ } as const;
133
+ } else {
134
+ mut_grouped.typeLiterals.nodes.push(t);
135
+ }
136
+
137
+ continue;
138
+ }
139
+
140
+ // intersections, unions, etc.
141
+ if (mut_grouped.others === undefined) {
142
+ mut_grouped.others = {
143
+ firstPosition: i,
144
+ nodes: [t],
145
+ } as const;
146
+ } else {
147
+ mut_grouped.others.nodes.push(t);
148
+ }
149
+ }
150
+
151
+ return mut_grouped;
152
+ };
@@ -0,0 +1,4 @@
1
+ export * from './compare-union-types.mjs';
2
+ export * from './constants.mjs';
3
+ export * from './group-union-types.mjs';
4
+ export * from './readonly-context.mjs';
@@ -0,0 +1,65 @@
1
+ import { SafeUint, match } from 'ts-data-forge';
2
+
3
+ /**
4
+ * Controls whether to make a layer mutable during recursive transformation
5
+ * calls, to standardize by omitting readonly from inner `number[]` in types
6
+ * like `DeepReadonly<[string, number[]]>`.
7
+ *
8
+ * - `"DeepReadonly"`: Indicates that `node` is inside a type utility like
9
+ * `DeepReadonly` that recursively applies readonly.
10
+ * - `"Readonly"`: Indicates that `node` is directly under a `Readonly` type.
11
+ * - `"readonly"`: Indicates that `node` is directly under a `readonly` operator.
12
+ * - `"IndexedAccessObjectType"`: Indicates that `node` is directly under an
13
+ * IndexedAccessTypeNode.
14
+ * - `"none"`: All other cases
15
+ */
16
+ export type ReadonlyContext = Readonly<
17
+ { indexedAccessDepth: SafeUintWithSmallInt } & (
18
+ | { type: 'DeepReadonly' }
19
+ | { type: 'Readonly' }
20
+ | { type: 'readonly' }
21
+ | { type: 'none' }
22
+ )
23
+ >;
24
+
25
+ export const nextReadonlyContext = <
26
+ const Curr extends ReadonlyContext,
27
+ const Next extends ReadonlyContext['type'],
28
+ >({
29
+ currentReadonlyContext: curr,
30
+ nextReadonlyContextType: next,
31
+ indexedAccessDepthChange = 'infinity',
32
+ }: Readonly<
33
+ {
34
+ currentReadonlyContext: Curr;
35
+ } & (
36
+ | {
37
+ nextReadonlyContextType: Next;
38
+ indexedAccessDepthChange: 'decr' | 'incr' | 'keep' | 'infinity';
39
+ }
40
+ | {
41
+ nextReadonlyContextType: Extract<
42
+ ReadonlyContext['type'],
43
+ 'DeepReadonly'
44
+ >;
45
+ indexedAccessDepthChange?: 'infinity';
46
+ }
47
+ )
48
+ >): Readonly<{
49
+ type: Extract<ReadonlyContext['type'], 'DeepReadonly'> | Next;
50
+ indexedAccessDepth: SafeUintWithSmallInt;
51
+ }> =>
52
+ curr.type === 'DeepReadonly'
53
+ ? {
54
+ type: 'DeepReadonly',
55
+ indexedAccessDepth: SafeUint.MAX_VALUE,
56
+ }
57
+ : {
58
+ type: next,
59
+ indexedAccessDepth: match(indexedAccessDepthChange, {
60
+ decr: SafeUint.sub(curr.indexedAccessDepth, 1),
61
+ keep: curr.indexedAccessDepth,
62
+ incr: SafeUint.add(curr.indexedAccessDepth, 1),
63
+ infinity: SafeUint.MAX_VALUE,
64
+ }),
65
+ };
@@ -0,0 +1,238 @@
1
+ /* eslint-disable unicorn/consistent-function-scoping -- helper functions are kept inside for clarity */
2
+ /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- ts-morph uses mutable types */
3
+ import { Arr } from 'ts-data-forge';
4
+ import * as tsm from 'ts-morph';
5
+ import { type TsMorphTransformer } from './types.mjs';
6
+
7
+ /**
8
+ * Replaces `Readonly<Record<string, unknown>>`, `Record<string, unknown>`,
9
+ * and index signatures `[k: string]: unknown` with `UnknownRecord`
10
+ */
11
+ export const replaceRecordWithUnknownRecordTransformer =
12
+ (): TsMorphTransformer => (sourceAst) => {
13
+ const processDeclarations = (
14
+ container: tsm.SourceFile | tsm.ModuleDeclaration,
15
+ ): void => {
16
+ const typeAliases = container.getTypeAliases();
17
+
18
+ for (const typeAlias of typeAliases) {
19
+ const typeNode = typeAlias.getTypeNode();
20
+
21
+ if (typeNode === undefined) continue;
22
+
23
+ visitTypeNode(typeNode);
24
+ }
25
+
26
+ const interfaces = container.getInterfaces();
27
+
28
+ for (const interfaceDecl of interfaces) {
29
+ // Check if interface has index signature [k: string]: unknown
30
+ const indexSignatures = interfaceDecl.getIndexSignatures();
31
+
32
+ const hasStringUnknownSignature = indexSignatures.some((sig) => {
33
+ const keyType = sig.getKeyType();
34
+
35
+ const returnType = sig.getReturnType();
36
+
37
+ return (
38
+ keyType.getText() === 'string' && returnType.getText() === 'unknown'
39
+ );
40
+ });
41
+
42
+ // If it has the signature and no other members, replace entire interface with type alias
43
+ if (hasStringUnknownSignature) {
44
+ const properties = interfaceDecl.getProperties();
45
+
46
+ if (properties.length === 0 && indexSignatures.length === 1) {
47
+ // Replace interface with type alias
48
+ const interfaceName = interfaceDecl.getName();
49
+
50
+ interfaceDecl.replaceWithText(
51
+ `export type ${interfaceName} = UnknownRecord;`,
52
+ );
53
+
54
+ continue;
55
+ }
56
+ }
57
+
58
+ // Otherwise, check properties for Record types
59
+ for (const property of interfaceDecl.getProperties()) {
60
+ const typeNode = property.getTypeNode();
61
+
62
+ if (typeNode === undefined) continue;
63
+
64
+ visitTypeNode(typeNode);
65
+ }
66
+ }
67
+ };
68
+
69
+ const visitTypeNode = (node: tsm.TypeNode): void => {
70
+ if (tsm.Node.isTypeReference(node)) {
71
+ // Check if it's Readonly<{ [k: string]: unknown }>
72
+ if (node.getTypeName().getText() === 'Readonly') {
73
+ const typeArgs = node.getTypeArguments();
74
+
75
+ if (typeArgs.length === 1) {
76
+ const typeArg = typeArgs[0];
77
+
78
+ if (tsm.Node.isTypeLiteral(typeArg)) {
79
+ const members = typeArg.getMembers();
80
+
81
+ const indexSigs = members.filter((m) =>
82
+ tsm.Node.isIndexSignatureDeclaration(m),
83
+ );
84
+
85
+ // Check if it has only one index signature [k: string]: unknown
86
+ if (
87
+ members.length === 1 &&
88
+ Arr.isArrayOfLength(indexSigs, 1) &&
89
+ isStringUnknownIndexSignature(indexSigs[0])
90
+ ) {
91
+ node.replaceWithText('UnknownRecord');
92
+
93
+ return;
94
+ }
95
+
96
+ // Otherwise, recurse into the type literal to visit its properties
97
+ visitTypeNode(typeArg);
98
+
99
+ return;
100
+ }
101
+ }
102
+ }
103
+
104
+ replaceIfRecordUnknown(node);
105
+ }
106
+
107
+ // Check for type literal { [k: string]: unknown }
108
+ if (tsm.Node.isTypeLiteral(node)) {
109
+ const members = node.getMembers();
110
+
111
+ const indexSigs = members.filter((m) =>
112
+ tsm.Node.isIndexSignatureDeclaration(m),
113
+ );
114
+
115
+ // Check if it has only one index signature [k: string]: unknown
116
+ if (
117
+ members.length === 1 &&
118
+ Arr.isArrayOfLength(indexSigs, 1) &&
119
+ isStringUnknownIndexSignature(indexSigs[0])
120
+ ) {
121
+ node.replaceWithText('UnknownRecord');
122
+
123
+ return;
124
+ }
125
+ }
126
+
127
+ // Recursively visit child type nodes
128
+ if (tsm.Node.isUnionTypeNode(node)) {
129
+ for (const typeNode of node.getTypeNodes()) {
130
+ visitTypeNode(typeNode);
131
+ }
132
+ } else if (tsm.Node.isIntersectionTypeNode(node)) {
133
+ for (const typeNode of node.getTypeNodes()) {
134
+ visitTypeNode(typeNode);
135
+ }
136
+ } else if (tsm.Node.isArrayTypeNode(node)) {
137
+ visitTypeNode(node.getElementTypeNode());
138
+ } else if (tsm.Node.isTupleTypeNode(node)) {
139
+ for (const element of node.getElements()) {
140
+ visitTypeNode(element);
141
+ }
142
+ } else if (tsm.Node.isParenthesizedTypeNode(node)) {
143
+ visitTypeNode(node.getTypeNode());
144
+ } else if (tsm.Node.isTypeLiteral(node)) {
145
+ for (const member of node.getMembers()) {
146
+ if (tsm.Node.isPropertySignature(member)) {
147
+ const typeNode = member.getTypeNode();
148
+
149
+ if (typeNode !== undefined) {
150
+ visitTypeNode(typeNode);
151
+ }
152
+ } else if (tsm.Node.isIndexSignatureDeclaration(member)) {
153
+ const typeNode = member.getReturnTypeNode();
154
+
155
+ if (typeNode !== undefined) {
156
+ visitTypeNode(typeNode);
157
+ }
158
+ }
159
+ }
160
+ }
161
+ };
162
+
163
+ const isStringUnknownIndexSignature = (
164
+ sig: tsm.IndexSignatureDeclaration,
165
+ ): boolean => {
166
+ const keyType = sig.getKeyTypeNode();
167
+
168
+ const returnType = sig.getReturnTypeNode();
169
+
170
+ return (
171
+ keyType.getText() === 'string' && returnType?.getText() === 'unknown'
172
+ );
173
+ };
174
+
175
+ const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode): void => {
176
+ const typeName = node.getTypeName().getText();
177
+
178
+ switch (typeName) {
179
+ case 'Record': {
180
+ // Check if it's Record<string, unknown>
181
+ const typeArgs = node.getTypeArguments();
182
+
183
+ if (
184
+ typeArgs.length === 2 &&
185
+ typeArgs[0]?.getText() === 'string' &&
186
+ typeArgs[1]?.getText() === 'unknown'
187
+ ) {
188
+ node.replaceWithText('UnknownRecord');
189
+ }
190
+
191
+ break;
192
+ }
193
+ case 'Readonly': {
194
+ // Check if it's Readonly<Record<string, unknown>>
195
+ const typeArgs = node.getTypeArguments();
196
+
197
+ if (typeArgs.length === 1) {
198
+ const innerType = typeArgs[0];
199
+
200
+ if (
201
+ innerType !== undefined &&
202
+ tsm.Node.isTypeReference(innerType)
203
+ ) {
204
+ const innerTypeName = innerType.getTypeName().getText();
205
+
206
+ if (innerTypeName === 'Record') {
207
+ const innerTypeArgs = innerType.getTypeArguments();
208
+
209
+ if (
210
+ innerTypeArgs.length === 2 &&
211
+ innerTypeArgs[0]?.getText() === 'string' &&
212
+ innerTypeArgs[1]?.getText() === 'unknown'
213
+ ) {
214
+ node.replaceWithText('UnknownRecord');
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ break;
221
+ }
222
+
223
+ default: {
224
+ break;
225
+ }
226
+ }
227
+ };
228
+
229
+ // Process top-level declarations
230
+ processDeclarations(sourceAst);
231
+
232
+ // Process declarations inside namespaces/modules
233
+ const namespaces = sourceAst.getModules();
234
+
235
+ for (const namespace of namespaces) {
236
+ processDeclarations(namespace);
237
+ }
238
+ };
@@ -0,0 +1,38 @@
1
+ import * as tsm from 'ts-morph';
2
+ import { IGNORE_FILE_COMMENT_TEXT } from '../constants/index.mjs';
3
+ import { type TsMorphTransformer } from './types.mjs';
4
+
5
+ export const transformSourceCode = (
6
+ code: string,
7
+ isTsx: boolean,
8
+ transformers: readonly TsMorphTransformer[],
9
+ debug: boolean = false,
10
+ ): string => {
11
+ if (code.includes(IGNORE_FILE_COMMENT_TEXT)) {
12
+ if (debug) {
13
+ console.debug('skipped by ignore-file comment');
14
+ }
15
+
16
+ return code;
17
+ }
18
+
19
+ const project = new tsm.Project({
20
+ useInMemoryFileSystem: true,
21
+ compilerOptions: {
22
+ jsx: isTsx ? tsm.ts.JsxEmit.React : undefined,
23
+ target: tsm.ts.ScriptTarget.ESNext,
24
+ module: tsm.ts.ModuleKind.ESNext,
25
+ },
26
+ });
27
+
28
+ const sourceAst = project.createSourceFile(
29
+ `source.${isTsx ? 'tsx' : 'ts'}`,
30
+ code,
31
+ );
32
+
33
+ for (const transformer of transformers) {
34
+ transformer(sourceAst);
35
+ }
36
+
37
+ return sourceAst.getFullText();
38
+ };
@@ -0,0 +1,6 @@
1
+ import type * as tsm from 'ts-morph';
2
+
3
+ export type TsMorphTransformer = (
4
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
5
+ sourceFile: tsm.SourceFile,
6
+ ) => void;
@@ -0,0 +1,3 @@
1
+ export const IGNORE_LINE_COMMENT_TEXT = 'transformer-ignore-next-line';
2
+
3
+ export const IGNORE_FILE_COMMENT_TEXT = '/* transformer-ignore */';
@@ -0,0 +1 @@
1
+ export * from './ignore-comment-text.mjs';
@@ -0,0 +1,56 @@
1
+ import * as tsm from 'ts-morph';
2
+ import { IGNORE_LINE_COMMENT_TEXT } from '../constants/index.mjs';
3
+
4
+ /**
5
+ * Checks if a given ts-morph Node is immediately preceded by a
6
+ * '// transformer-ignore-next-line' comment.
7
+ *
8
+ * @param node - The ts-morph Node to check.
9
+ * @returns True if the node is preceded by the ignore comment on the immediately previous line, false otherwise.
10
+ */
11
+ export const hasDisableNextLineComment = (node: tsm.Node): boolean => {
12
+ const nodeStartLine = node.getStartLineNumber();
13
+
14
+ // Cannot be ignored if it's on the first line
15
+ if (nodeStartLine <= 1) {
16
+ return false;
17
+ }
18
+
19
+ const sourceFile = node.getSourceFile(); // Get the SourceFile
20
+
21
+ const leadingCommentRanges = node.getLeadingCommentRanges();
22
+
23
+ // Iterate backwards through comments as the closest one is most relevant
24
+ for (let mut_i = leadingCommentRanges.length - 1; mut_i >= 0; mut_i--) {
25
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
26
+ const commentRange = leadingCommentRanges[mut_i]!;
27
+
28
+ // Get the end position and convert it to line number
29
+ const commentEndPos = commentRange.getEnd();
30
+
31
+ const commentEndLine = sourceFile.getLineAndColumnAtPos(commentEndPos).line;
32
+
33
+ // Check if the comment is on the immediately preceding line
34
+ if (nodeStartLine === commentEndLine + 1) {
35
+ // Check if it's a single-line comment containing the specific ignore text
36
+ if (
37
+ commentRange.getKind() === tsm.SyntaxKind.SingleLineCommentTrivia &&
38
+ commentRange.getText().trim().includes(IGNORE_LINE_COMMENT_TEXT)
39
+ ) {
40
+ return true;
41
+ }
42
+
43
+ // If we found *any* comment on the preceding line, but it wasn't
44
+ // the correct ignore comment, then the node is not ignored by
45
+ // a comment on the *immediately* preceding line. Stop checking further back.
46
+ return false;
47
+ }
48
+
49
+ // If the comment ends before the preceding line, stop checking further back.
50
+ if (commentEndLine < nodeStartLine - 1) {
51
+ break;
52
+ }
53
+ }
54
+
55
+ return false;
56
+ };
@@ -0,0 +1,8 @@
1
+ export * from './has-disable-next-line-comment.mjs';
2
+ export * from './is-as-const-node.mjs';
3
+ export * from './is-primitive-type-node.mjs';
4
+ export * from './is-readonly-node.mjs';
5
+ export * from './is-spread-parameter-node.mjs';
6
+ export * from './remove-parentheses.mjs';
7
+ export * from './unwrap-readonly.mjs';
8
+ export * from './wrap-with-parentheses.mjs';
@@ -0,0 +1,47 @@
1
+ import * as ts from 'ts-morph';
2
+
3
+ export const isAsConstNode = (
4
+ node: ts.Node,
5
+ ): node is ts.AsExpression &
6
+ Readonly<{
7
+ type: ts.TypeReferenceNode &
8
+ Readonly<{
9
+ typeName: ts.Identifier &
10
+ Readonly<{
11
+ text: 'const';
12
+ }>;
13
+ typeArguments: undefined;
14
+ }>;
15
+ }> => {
16
+ if (!node.isKind(ts.SyntaxKind.AsExpression)) {
17
+ return false;
18
+ }
19
+
20
+ // 2. Get the 'type' node from the AsExpression
21
+ const typeNode = node.getTypeNode();
22
+
23
+ if (typeNode === undefined) {
24
+ return false; // Should have a type node for 'as const'
25
+ }
26
+
27
+ // 3. Check if the 'type' node is a TypeReference
28
+ if (!typeNode.isKind(ts.SyntaxKind.TypeReference)) {
29
+ return false;
30
+ }
31
+
32
+ // 4. Get the 'typeName' from the TypeReference
33
+ const typeNameNode = typeNode.getTypeName();
34
+
35
+ // 5. Check if the 'typeName' is an Identifier
36
+ if (!typeNameNode.isKind(ts.SyntaxKind.Identifier)) {
37
+ // 'as const' uses a simple Identifier 'const', not a QualifiedName
38
+ return false;
39
+ }
40
+
41
+ // 6. Check if the Identifier's text is 'const'
42
+ // and that there are no type arguments (as const doesn't have them)
43
+ return (
44
+ typeNameNode.getText() === 'const' &&
45
+ typeNode.getTypeArguments().length === 0
46
+ );
47
+ };