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.
- package/LICENSE +201 -0
- package/README.md +33 -0
- package/dist/cmd/convert-to-readonly.d.mts +3 -0
- package/dist/cmd/convert-to-readonly.d.mts.map +1 -0
- package/dist/cmd/convert-to-readonly.mjs +137 -0
- package/dist/cmd/convert-to-readonly.mjs.map +1 -0
- package/dist/entry-point.d.mts +2 -0
- package/dist/entry-point.d.mts.map +1 -0
- package/dist/entry-point.mjs +19 -0
- package/dist/entry-point.mjs.map +1 -0
- package/dist/functions/ast-transformers/convert-interface-to-type.d.mts +7 -0
- package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -0
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs +83 -0
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -0
- package/dist/functions/ast-transformers/convert-to-readonly-type.d.mts +29 -0
- package/dist/functions/ast-transformers/convert-to-readonly-type.d.mts.map +1 -0
- package/dist/functions/ast-transformers/convert-to-readonly-type.mjs +811 -0
- package/dist/functions/ast-transformers/convert-to-readonly-type.mjs.map +1 -0
- package/dist/functions/ast-transformers/index.d.mts +7 -0
- package/dist/functions/ast-transformers/index.d.mts.map +1 -0
- package/dist/functions/ast-transformers/index.mjs +9 -0
- package/dist/functions/ast-transformers/index.mjs.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.d.mts +3 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.d.mts.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs +22 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.d.mts +2 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.d.mts.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs +15 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts +21 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +72 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.d.mts +5 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.d.mts.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs +5 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts +37 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs +19 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs.map +1 -0
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +7 -0
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts.map +1 -0
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +173 -0
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -0
- package/dist/functions/ast-transformers/transform-source-code.d.mts +3 -0
- package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -0
- package/dist/functions/ast-transformers/transform-source-code.mjs +27 -0
- package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -0
- package/dist/functions/ast-transformers/types.d.mts +3 -0
- package/dist/functions/ast-transformers/types.d.mts.map +1 -0
- package/dist/functions/ast-transformers/types.mjs +2 -0
- package/dist/functions/ast-transformers/types.mjs.map +1 -0
- package/dist/functions/constants/ignore-comment-text.d.mts +3 -0
- package/dist/functions/constants/ignore-comment-text.d.mts.map +1 -0
- package/dist/functions/constants/ignore-comment-text.mjs +5 -0
- package/dist/functions/constants/ignore-comment-text.mjs.map +1 -0
- package/dist/functions/constants/index.d.mts +2 -0
- package/dist/functions/constants/index.d.mts.map +1 -0
- package/dist/functions/constants/index.mjs +2 -0
- package/dist/functions/constants/index.mjs.map +1 -0
- package/dist/functions/functions/has-disable-next-line-comment.d.mts +10 -0
- package/dist/functions/functions/has-disable-next-line-comment.d.mts.map +1 -0
- package/dist/functions/functions/has-disable-next-line-comment.mjs +47 -0
- package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -0
- package/dist/functions/functions/index.d.mts +9 -0
- package/dist/functions/functions/index.d.mts.map +1 -0
- package/dist/functions/functions/index.mjs +9 -0
- package/dist/functions/functions/index.mjs.map +1 -0
- package/dist/functions/functions/is-as-const-node.d.mts +10 -0
- package/dist/functions/functions/is-as-const-node.d.mts.map +1 -0
- package/dist/functions/functions/is-as-const-node.mjs +30 -0
- package/dist/functions/functions/is-as-const-node.mjs.map +1 -0
- package/dist/functions/functions/is-primitive-type-node.d.mts +15 -0
- package/dist/functions/functions/is-primitive-type-node.d.mts.map +1 -0
- package/dist/functions/functions/is-primitive-type-node.mjs +46 -0
- package/dist/functions/functions/is-primitive-type-node.mjs.map +1 -0
- package/dist/functions/functions/is-readonly-node.d.mts +21 -0
- package/dist/functions/functions/is-readonly-node.d.mts.map +1 -0
- package/dist/functions/functions/is-readonly-node.mjs +30 -0
- package/dist/functions/functions/is-readonly-node.mjs.map +1 -0
- package/dist/functions/functions/is-spread-parameter-node.d.mts +4 -0
- package/dist/functions/functions/is-spread-parameter-node.d.mts.map +1 -0
- package/dist/functions/functions/is-spread-parameter-node.mjs +9 -0
- package/dist/functions/functions/is-spread-parameter-node.mjs.map +1 -0
- package/dist/functions/functions/remove-parentheses.d.mts +3 -0
- package/dist/functions/functions/remove-parentheses.d.mts.map +1 -0
- package/dist/functions/functions/remove-parentheses.mjs +9 -0
- package/dist/functions/functions/remove-parentheses.mjs.map +1 -0
- package/dist/functions/functions/unwrap-readonly.d.mts +3 -0
- package/dist/functions/functions/unwrap-readonly.d.mts.map +1 -0
- package/dist/functions/functions/unwrap-readonly.mjs +8 -0
- package/dist/functions/functions/unwrap-readonly.mjs.map +1 -0
- package/dist/functions/functions/wrap-with-parentheses.d.mts +2 -0
- package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -0
- package/dist/functions/functions/wrap-with-parentheses.mjs +4 -0
- package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -0
- package/dist/functions/index.d.mts +5 -0
- package/dist/functions/index.d.mts.map +1 -0
- package/dist/functions/index.mjs +19 -0
- package/dist/functions/index.mjs.map +1 -0
- package/dist/functions/utils/index.d.mts +2 -0
- package/dist/functions/utils/index.d.mts.map +1 -0
- package/dist/functions/utils/index.mjs +2 -0
- package/dist/functions/utils/index.mjs.map +1 -0
- package/dist/functions/utils/replace-with-debug.d.mts +3 -0
- package/dist/functions/utils/replace-with-debug.d.mts.map +1 -0
- package/dist/functions/utils/replace-with-debug.mjs +7 -0
- package/dist/functions/utils/replace-with-debug.mjs.map +1 -0
- package/dist/globals.d.mts +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +19 -0
- package/dist/index.mjs.map +1 -0
- package/dist/tsconfig.json +1 -0
- package/dist/types.d.mts +2 -0
- package/package.json +134 -0
- package/src/cmd/convert-to-readonly.mts +195 -0
- package/src/entry-point.mts +1 -0
- package/src/functions/ast-transformers/convert-interface-to-type.mts +119 -0
- package/src/functions/ast-transformers/convert-interface-to-type.test.mts +295 -0
- package/src/functions/ast-transformers/convert-to-readonly-type.mts +1391 -0
- package/src/functions/ast-transformers/convert-to-readonly-type.test.mts +3653 -0
- package/src/functions/ast-transformers/index.mts +6 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mts +24 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/constants.mts +12 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +152 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/index.mts +4 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mts +65 -0
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +238 -0
- package/src/functions/ast-transformers/transform-source-code.mts +38 -0
- package/src/functions/ast-transformers/types.mts +6 -0
- package/src/functions/constants/ignore-comment-text.mts +3 -0
- package/src/functions/constants/index.mts +1 -0
- package/src/functions/functions/has-disable-next-line-comment.mts +56 -0
- package/src/functions/functions/index.mts +8 -0
- package/src/functions/functions/is-as-const-node.mts +47 -0
- package/src/functions/functions/is-primitive-type-node.mts +301 -0
- package/src/functions/functions/is-readonly-node.mts +247 -0
- package/src/functions/functions/is-spread-parameter-node.mts +13 -0
- package/src/functions/functions/remove-parentheses.mts +7 -0
- package/src/functions/functions/unwrap-readonly.mts +7 -0
- package/src/functions/functions/wrap-with-parentheses.mts +2 -0
- package/src/functions/index.mts +4 -0
- package/src/functions/utils/index.mts +1 -0
- package/src/functions/utils/replace-with-debug.mts +10 -0
- package/src/globals.d.mts +1 -0
- 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,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,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 @@
|
|
|
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
|
+
};
|