ts-codemod-lib 2.0.4 → 2.0.5

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 (60) hide show
  1. package/dist/cmd/append-as-const.mjs +1 -1
  2. package/dist/cmd/convert-interface-to-type.mjs +1 -1
  3. package/dist/cmd/convert-to-readonly.mjs +1 -1
  4. package/dist/cmd/replace-any-with-unknown.mjs +1 -1
  5. package/dist/cmd/replace-record-with-unknown-record.mjs +1 -1
  6. package/dist/cmd/run-transformer-cli.mjs +3 -3
  7. package/dist/cmd/run-transformer-cli.mjs.map +1 -1
  8. package/dist/entry-point.mjs +1 -0
  9. package/dist/entry-point.mjs.map +1 -1
  10. package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -1
  11. package/dist/functions/ast-transformers/convert-interface-to-type.mjs +4 -4
  12. package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -1
  13. package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
  14. package/dist/functions/ast-transformers/convert-to-readonly.mjs +31 -12
  15. package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
  16. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs +1 -0
  17. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs.map +1 -1
  18. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +1 -0
  19. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
  20. package/dist/functions/ast-transformers/replace-any-with-unknown.mjs +1 -1
  21. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +13 -12
  22. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
  23. package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -1
  24. package/dist/functions/ast-transformers/transform-source-code.mjs +3 -2
  25. package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
  26. package/dist/functions/functions/index.d.mts +1 -0
  27. package/dist/functions/functions/index.d.mts.map +1 -1
  28. package/dist/functions/functions/index.mjs +1 -0
  29. package/dist/functions/functions/index.mjs.map +1 -1
  30. package/dist/functions/functions/is-as-const-node.d.mts.map +1 -1
  31. package/dist/functions/functions/is-as-const-node.mjs +2 -1
  32. package/dist/functions/functions/is-as-const-node.mjs.map +1 -1
  33. package/dist/functions/functions/should-avoid-parentheses-for-readonly.d.mts +16 -0
  34. package/dist/functions/functions/should-avoid-parentheses-for-readonly.d.mts.map +1 -0
  35. package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs +32 -0
  36. package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs.map +1 -0
  37. package/dist/functions/functions/wrap-with-parentheses.d.mts +13 -1
  38. package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
  39. package/dist/functions/functions/wrap-with-parentheses.mjs +58 -1
  40. package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
  41. package/dist/functions/index.mjs +1 -0
  42. package/dist/functions/index.mjs.map +1 -1
  43. package/dist/index.mjs +1 -0
  44. package/dist/index.mjs.map +1 -1
  45. package/package.json +8 -8
  46. package/src/cmd/append-as-const.mts +1 -1
  47. package/src/cmd/convert-interface-to-type.mts +1 -1
  48. package/src/cmd/convert-to-readonly.mts +1 -1
  49. package/src/cmd/replace-any-with-unknown.mts +1 -1
  50. package/src/cmd/replace-record-with-unknown-record.mts +1 -1
  51. package/src/cmd/run-transformer-cli.mts +3 -3
  52. package/src/functions/ast-transformers/convert-interface-to-type.mts +6 -7
  53. package/src/functions/ast-transformers/convert-to-readonly.mts +39 -13
  54. package/src/functions/ast-transformers/convert-to-readonly.test.mts +79 -0
  55. package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +15 -12
  56. package/src/functions/ast-transformers/transform-source-code.mts +3 -2
  57. package/src/functions/functions/index.mts +1 -0
  58. package/src/functions/functions/is-as-const-node.mts +2 -1
  59. package/src/functions/functions/should-avoid-parentheses-for-readonly.mts +32 -0
  60. package/src/functions/functions/wrap-with-parentheses.mts +75 -2
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../../src/functions/functions/index.mts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../../src/functions/functions/index.mts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,6CAA6C,CAAC;AAC5D,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC"}
@@ -4,6 +4,7 @@ export { isAtomicTypeNode } from './is-atomic-type-node.mjs';
4
4
  export { isReadonlyArrayTypeNode, isReadonlyTupleOrArrayTypeNode, isReadonlyTupleTypeNode, isReadonlyTypeReferenceNode, isShallowReadonlyTypeNode } from './is-readonly-node.mjs';
5
5
  export { isSpreadNamedTupleMemberNode, isSpreadParameterNode } from './is-spread-parameter-node.mjs';
6
6
  export { removeParentheses } from './remove-parentheses.mjs';
7
+ export { shouldAvoidParenthesesForReadonly } from './should-avoid-parentheses-for-readonly.mjs';
7
8
  export { unwrapReadonlyTypeArgText } from './unwrap-readonly.mjs';
8
9
  export { wrapWithParentheses } from './wrap-with-parentheses.mjs';
9
10
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;"}
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"is-as-const-node.d.mts","sourceRoot":"","sources":["../../../src/functions/functions/is-as-const-node.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,UAAU,CAAC;AAE/B,eAAO,MAAM,aAAa,GACxB,MAAM,EAAE,CAAC,IAAI,KACZ,IAAI,IAAI,EAAE,CAAC,YAAY,GACxB,QAAQ,CAAC;IACP,IAAI,EAAE,EAAE,CAAC,iBAAiB,GACxB,QAAQ,CAAC;QACP,QAAQ,EAAE,EAAE,CAAC,UAAU,GACrB,QAAQ,CAAC;YACP,IAAI,EAAE,OAAO,CAAC;SACf,CAAC,CAAC;QACL,aAAa,EAAE,SAAS,CAAC;KAC1B,CAAC,CAAC;CACN,CAgCF,CAAC"}
1
+ {"version":3,"file":"is-as-const-node.d.mts","sourceRoot":"","sources":["../../../src/functions/functions/is-as-const-node.mts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,UAAU,CAAC;AAE/B,eAAO,MAAM,aAAa,GACxB,MAAM,EAAE,CAAC,IAAI,KACZ,IAAI,IAAI,EAAE,CAAC,YAAY,GACxB,QAAQ,CAAC;IACP,IAAI,EAAE,EAAE,CAAC,iBAAiB,GACxB,QAAQ,CAAC;QACP,QAAQ,EAAE,EAAE,CAAC,UAAU,GACrB,QAAQ,CAAC;YACP,IAAI,EAAE,OAAO,CAAC;SACf,CAAC,CAAC;QACL,aAAa,EAAE,SAAS,CAAC;KAC1B,CAAC,CAAC;CACN,CAgCF,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { Arr } from 'ts-data-forge';
1
2
  import * as tsm from 'ts-morph';
2
3
 
3
4
  const isAsConstNode = (node) => {
@@ -23,7 +24,7 @@ const isAsConstNode = (node) => {
23
24
  // 6. Check if the Identifier's text is 'const'
24
25
  // and that there are no type arguments (as const doesn't have them)
25
26
  return (typeNameNode.getText() === 'const' &&
26
- typeNode.getTypeArguments().length === 0);
27
+ Arr.isArrayOfLength(typeNode.getTypeArguments(), 0));
27
28
  };
28
29
 
29
30
  export { isAsConstNode };
@@ -1 +1 @@
1
- {"version":3,"file":"is-as-const-node.mjs","sources":["../../../src/functions/functions/is-as-const-node.mts"],"sourcesContent":[null],"names":["ts"],"mappings":";;AAEO,MAAM,aAAa,GAAG,CAC3B,IAAa,KAWR;AACL,IAAA,IAAI,CAAC,IAAI,CAAC,MAAM,CAACA,GAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE;AAC5C,QAAA,OAAO,KAAK;IACd;;AAGA,IAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE;AAEnC,IAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;QAC1B,OAAO,KAAK,CAAC;IACf;;AAGA,IAAA,IAAI,CAAC,QAAQ,CAAC,MAAM,CAACA,GAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE;AACjD,QAAA,OAAO,KAAK;IACd;;AAGA,IAAA,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,EAAE;;AAG3C,IAAA,IAAI,CAAC,YAAY,CAAC,MAAM,CAACA,GAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE;;AAElD,QAAA,OAAO,KAAK;IACd;;;AAIA,IAAA,QACE,YAAY,CAAC,OAAO,EAAE,KAAK,OAAO;QAClC,QAAQ,CAAC,gBAAgB,EAAE,CAAC,MAAM,KAAK,CAAC;AAE5C;;;;"}
1
+ {"version":3,"file":"is-as-const-node.mjs","sources":["../../../src/functions/functions/is-as-const-node.mts"],"sourcesContent":[null],"names":["ts"],"mappings":";;;AAGO,MAAM,aAAa,GAAG,CAC3B,IAAa,KAWR;AACL,IAAA,IAAI,CAAC,IAAI,CAAC,MAAM,CAACA,GAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE;AAC5C,QAAA,OAAO,KAAK;IACd;;AAGA,IAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE;AAEnC,IAAA,IAAI,QAAQ,KAAK,SAAS,EAAE;QAC1B,OAAO,KAAK,CAAC;IACf;;AAGA,IAAA,IAAI,CAAC,QAAQ,CAAC,MAAM,CAACA,GAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE;AACjD,QAAA,OAAO,KAAK;IACd;;AAGA,IAAA,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,EAAE;;AAG3C,IAAA,IAAI,CAAC,YAAY,CAAC,MAAM,CAACA,GAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE;;AAElD,QAAA,OAAO,KAAK;IACd;;;AAIA,IAAA,QACE,YAAY,CAAC,OAAO,EAAE,KAAK,OAAO;QAClC,GAAG,CAAC,eAAe,CAAC,QAAQ,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;AAEvD;;;;"}
@@ -0,0 +1,16 @@
1
+ import * as tsm from 'ts-morph';
2
+ /**
3
+ * Determines if parentheses should be avoided when adding 'readonly' prefix to a type.
4
+ *
5
+ * Parentheses should be AVOIDED in these specific contexts:
6
+ * - Type predicates: `x is [T, U]` -> `x is readonly [T, U]` (not `x is (readonly [T, U])`)
7
+ * because parentheses around the type in a type predicate cause syntax errors
8
+ *
9
+ * For all other contexts, we keep parentheses for safety to avoid precedence issues,
10
+ * relying on prettier to remove unnecessary ones.
11
+ *
12
+ * @param node - The type node to check
13
+ * @returns true if parentheses should be avoided, false otherwise (keep parentheses)
14
+ */
15
+ export declare const shouldAvoidParenthesesForReadonly: (node: tsm.Node) => boolean;
16
+ //# sourceMappingURL=should-avoid-parentheses-for-readonly.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"should-avoid-parentheses-for-readonly.d.mts","sourceRoot":"","sources":["../../../src/functions/functions/should-avoid-parentheses-for-readonly.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAEhC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iCAAiC,GAAI,MAAM,GAAG,CAAC,IAAI,KAAG,OAgBlE,CAAC"}
@@ -0,0 +1,32 @@
1
+ import * as tsm from 'ts-morph';
2
+
3
+ /**
4
+ * Determines if parentheses should be avoided when adding 'readonly' prefix to a type.
5
+ *
6
+ * Parentheses should be AVOIDED in these specific contexts:
7
+ * - Type predicates: `x is [T, U]` -> `x is readonly [T, U]` (not `x is (readonly [T, U])`)
8
+ * because parentheses around the type in a type predicate cause syntax errors
9
+ *
10
+ * For all other contexts, we keep parentheses for safety to avoid precedence issues,
11
+ * relying on prettier to remove unnecessary ones.
12
+ *
13
+ * @param node - The type node to check
14
+ * @returns true if parentheses should be avoided, false otherwise (keep parentheses)
15
+ */
16
+ const shouldAvoidParenthesesForReadonly = (node) => {
17
+ const parent = node.getParent();
18
+ if (parent === undefined) {
19
+ return false;
20
+ }
21
+ // Check if parent is TypePredicate: x is T
22
+ // In this case, we MUST avoid parentheses: x is readonly T (not x is (readonly T))
23
+ // because `x is (readonly T)` is a syntax error
24
+ if (parent.isKind(tsm.SyntaxKind.TypePredicate)) {
25
+ return true;
26
+ }
27
+ // For all other cases, keep parentheses for safety
28
+ return false;
29
+ };
30
+
31
+ export { shouldAvoidParenthesesForReadonly };
32
+ //# sourceMappingURL=should-avoid-parentheses-for-readonly.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"should-avoid-parentheses-for-readonly.mjs","sources":["../../../src/functions/functions/should-avoid-parentheses-for-readonly.mts"],"sourcesContent":[null],"names":[],"mappings":";;AAEA;;;;;;;;;;;;AAYG;AACI,MAAM,iCAAiC,GAAG,CAAC,IAAc,KAAa;AAC3E,IAAA,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE;AAE/B,IAAA,IAAI,MAAM,KAAK,SAAS,EAAE;AACxB,QAAA,OAAO,KAAK;IACd;;;;IAKA,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE;AAC/C,QAAA,OAAO,IAAI;IACb;;AAGA,IAAA,OAAO,KAAK;AACd;;;;"}
@@ -1,2 +1,14 @@
1
- export declare const wrapWithParentheses: (nodeStr: string) => `(${string})`;
1
+ /**
2
+ * Wraps a string with parentheses if not already wrapped.
3
+ * Avoids adding redundant parentheses when the expression is already
4
+ * fully wrapped with balanced parentheses.
5
+ *
6
+ * @example
7
+ * wrapWithParentheses('A') // '(A)'
8
+ * wrapWithParentheses('(A)') // '(A)' (not '((A))')
9
+ * wrapWithParentheses('A | B') // '(A | B)'
10
+ * wrapWithParentheses('(A | B)') // '(A | B)' (not '((A | B))')
11
+ * wrapWithParentheses('(A) | (B)') // '((A) | (B))' (needs outer parens)
12
+ */
13
+ export declare const wrapWithParentheses: (nodeStr: string) => string;
2
14
  //# sourceMappingURL=wrap-with-parentheses.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wrap-with-parentheses.d.mts","sourceRoot":"","sources":["../../../src/functions/functions/wrap-with-parentheses.mts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,GAAI,SAAS,MAAM,KAAG,IAAI,MAAM,GAChC,CAAC"}
1
+ {"version":3,"file":"wrap-with-parentheses.d.mts","sourceRoot":"","sources":["../../../src/functions/functions/wrap-with-parentheses.mts"],"names":[],"mappings":"AAsDA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,MAAM,KAAG,MAQrD,CAAC"}
@@ -1,4 +1,61 @@
1
- const wrapWithParentheses = (nodeStr) => `(${nodeStr.trim()})`;
1
+ import { range, asUint32 } from 'ts-data-forge';
2
+
3
+ /**
4
+ * Checks if a string is already wrapped with a single pair of balanced parentheses
5
+ * that encompasses the entire expression.
6
+ *
7
+ * @example
8
+ * isWrappedWithParentheses('(A)') // true
9
+ * isWrappedWithParentheses('((A))') // true (outer pair wraps everything)
10
+ * isWrappedWithParentheses('(A) | (B)') // false (outer parens don't wrap everything)
11
+ * isWrappedWithParentheses('A') // false
12
+ */
13
+ const isWrappedWithParentheses = (str) => {
14
+ const trimmed = str.trim();
15
+ if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) {
16
+ return false;
17
+ }
18
+ // Check if the opening and closing parentheses are balanced
19
+ // and the opening parenthesis corresponds to the closing one
20
+ let mut_depth = 0;
21
+ for (const mut_i of range(0, asUint32(trimmed.length))) {
22
+ const char = trimmed.charAt(mut_i);
23
+ switch (char) {
24
+ case '(': {
25
+ mut_depth += 1;
26
+ break;
27
+ }
28
+ case ')': {
29
+ mut_depth -= 1;
30
+ // If we reach depth 0 before the end, the outer parentheses don't wrap everything
31
+ if (mut_depth === 0 && mut_i < trimmed.length - 1) {
32
+ return false;
33
+ }
34
+ break;
35
+ }
36
+ }
37
+ }
38
+ return true;
39
+ };
40
+ /**
41
+ * Wraps a string with parentheses if not already wrapped.
42
+ * Avoids adding redundant parentheses when the expression is already
43
+ * fully wrapped with balanced parentheses.
44
+ *
45
+ * @example
46
+ * wrapWithParentheses('A') // '(A)'
47
+ * wrapWithParentheses('(A)') // '(A)' (not '((A))')
48
+ * wrapWithParentheses('A | B') // '(A | B)'
49
+ * wrapWithParentheses('(A | B)') // '(A | B)' (not '((A | B))')
50
+ * wrapWithParentheses('(A) | (B)') // '((A) | (B))' (needs outer parens)
51
+ */
52
+ const wrapWithParentheses = (nodeStr) => {
53
+ const trimmed = nodeStr.trim();
54
+ if (isWrappedWithParentheses(trimmed)) {
55
+ return trimmed;
56
+ }
57
+ return `(${trimmed})`;
58
+ };
2
59
 
3
60
  export { wrapWithParentheses };
4
61
  //# sourceMappingURL=wrap-with-parentheses.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"wrap-with-parentheses.mjs","sources":["../../../src/functions/functions/wrap-with-parentheses.mts"],"sourcesContent":[null],"names":[],"mappings":"AAAO,MAAM,mBAAmB,GAAG,CAAC,OAAe,KACjD,IAAI,OAAO,CAAC,IAAI,EAAE;;;;"}
1
+ {"version":3,"file":"wrap-with-parentheses.mjs","sources":["../../../src/functions/functions/wrap-with-parentheses.mts"],"sourcesContent":[null],"names":[],"mappings":";;AAAA;;;;;;;;;AASG;AAGH,MAAM,wBAAwB,GAAG,CAAC,GAAW,KAAa;AACxD,IAAA,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE;AAE1B,IAAA,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;AACtD,QAAA,OAAO,KAAK;IACd;;;IAIA,IAAI,SAAS,GAAG,CAAC;AAEjB,IAAA,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE;QACtD,MAAM,IAAI,GAAW,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;QAE1C,QAAQ,IAAI;YACV,KAAK,GAAG,EAAE;gBACR,SAAS,IAAI,CAAC;gBAEd;YACF;YAEA,KAAK,GAAG,EAAE;gBACR,SAAS,IAAI,CAAC;;AAGd,gBAAA,IAAI,SAAS,KAAK,CAAC,IAAI,KAAK,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE;AACjD,oBAAA,OAAO,KAAK;gBACd;gBAEA;YACF;;IAOJ;AAEA,IAAA,OAAO,IAAI;AACb,CAAC;AAED;;;;;;;;;;;AAWG;AACI,MAAM,mBAAmB,GAAG,CAAC,OAAe,KAAY;AAC7D,IAAA,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE;AAE9B,IAAA,IAAI,wBAAwB,CAAC,OAAO,CAAC,EAAE;AACrC,QAAA,OAAO,OAAO;IAChB;IAEA,OAAO,CAAA,CAAA,EAAI,OAAO,CAAA,CAAA,CAAY;AAChC;;;;"}
@@ -15,6 +15,7 @@ export { isAtomicTypeNode } from './functions/is-atomic-type-node.mjs';
15
15
  export { isReadonlyArrayTypeNode, isReadonlyTupleOrArrayTypeNode, isReadonlyTupleTypeNode, isReadonlyTypeReferenceNode, isShallowReadonlyTypeNode } from './functions/is-readonly-node.mjs';
16
16
  export { isSpreadNamedTupleMemberNode, isSpreadParameterNode } from './functions/is-spread-parameter-node.mjs';
17
17
  export { removeParentheses } from './functions/remove-parentheses.mjs';
18
+ export { shouldAvoidParenthesesForReadonly } from './functions/should-avoid-parentheses-for-readonly.mjs';
18
19
  export { unwrapReadonlyTypeArgText } from './functions/unwrap-readonly.mjs';
19
20
  export { wrapWithParentheses } from './functions/wrap-with-parentheses.mjs';
20
21
  export { replaceNodeWithDebugPrint } from './utils/replace-with-debug.mjs';
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;"}
package/dist/index.mjs CHANGED
@@ -15,6 +15,7 @@ export { isAtomicTypeNode } from './functions/functions/is-atomic-type-node.mjs'
15
15
  export { isReadonlyArrayTypeNode, isReadonlyTupleOrArrayTypeNode, isReadonlyTupleTypeNode, isReadonlyTypeReferenceNode, isShallowReadonlyTypeNode } from './functions/functions/is-readonly-node.mjs';
16
16
  export { isSpreadNamedTupleMemberNode, isSpreadParameterNode } from './functions/functions/is-spread-parameter-node.mjs';
17
17
  export { removeParentheses } from './functions/functions/remove-parentheses.mjs';
18
+ export { shouldAvoidParenthesesForReadonly } from './functions/functions/should-avoid-parentheses-for-readonly.mjs';
18
19
  export { unwrapReadonlyTypeArgText } from './functions/functions/unwrap-readonly.mjs';
19
20
  export { wrapWithParentheses } from './functions/functions/wrap-with-parentheses.mjs';
20
21
  export { replaceNodeWithDebugPrint } from './functions/utils/replace-with-debug.mjs';
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-codemod-lib",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript",
@@ -99,7 +99,7 @@
99
99
  "z:vitest:node": "pnpm run z:vitest --project='Node.js'"
100
100
  },
101
101
  "dependencies": {
102
- "ts-data-forge": "6.4.0",
102
+ "ts-data-forge": "6.5.0",
103
103
  "ts-morph": "27.0.2"
104
104
  },
105
105
  "devDependencies": {
@@ -113,17 +113,17 @@
113
113
  "@semantic-release/github": "12.0.3",
114
114
  "@semantic-release/npm": "13.1.3",
115
115
  "@semantic-release/release-notes-generator": "14.1.0",
116
- "@types/node": "25.2.0",
116
+ "@types/node": "25.2.3",
117
117
  "@vitest/browser-playwright": "4.0.18",
118
118
  "@vitest/coverage-v8": "4.0.18",
119
119
  "@vitest/ui": "4.0.18",
120
120
  "cmd-ts": "0.14.3",
121
121
  "conventional-changelog-conventionalcommits": "9.1.0",
122
- "cspell": "9.6.3",
122
+ "cspell": "9.6.4",
123
123
  "dedent": "1.7.1",
124
124
  "eslint": "9.39.2",
125
- "eslint-config-typed": "4.6.1",
126
- "github-settings-as-code": "1.1.2",
125
+ "eslint-config-typed": "4.6.2",
126
+ "github-settings-as-code": "1.1.4",
127
127
  "jiti": "2.6.1",
128
128
  "markdownlint": "0.40.0",
129
129
  "markdownlint-cli2": "0.20.0",
@@ -134,7 +134,7 @@
134
134
  "prettier-plugin-packagejson": "3.0.0",
135
135
  "rollup": "4.57.1",
136
136
  "semantic-release": "25.0.3",
137
- "ts-repo-utils": "8.1.0",
137
+ "ts-repo-utils": "8.2.0",
138
138
  "ts-type-forge": "2.3.1",
139
139
  "tslib": "2.8.1",
140
140
  "tsx": "4.21.0",
@@ -160,7 +160,7 @@
160
160
  "optional": true
161
161
  }
162
162
  },
163
- "packageManager": "pnpm@10.29.2",
163
+ "packageManager": "pnpm@10.29.3",
164
164
  "engines": {
165
165
  "node": ">=20.11.0",
166
166
  "pnpm": ">=8.0.0"
@@ -9,7 +9,7 @@ const transformer = appendAsConstTransformer();
9
9
 
10
10
  const cmdDef = cmd.command({
11
11
  name: transformer.name,
12
- version: '2.0.4',
12
+ version: '2.0.5',
13
13
  args: {
14
14
  baseDir: cmd.positional({
15
15
  type: cmd.string,
@@ -9,7 +9,7 @@ const transformer = convertInterfaceToTypeTransformer();
9
9
 
10
10
  const cmdDef = cmd.command({
11
11
  name: transformer.name,
12
- version: '2.0.4',
12
+ version: '2.0.5',
13
13
  args: {
14
14
  baseDir: cmd.positional({
15
15
  type: cmd.string,
@@ -9,7 +9,7 @@ const transformer = convertToReadonlyTransformer();
9
9
 
10
10
  const cmdDef = cmd.command({
11
11
  name: transformer.name,
12
- version: '2.0.4',
12
+ version: '2.0.5',
13
13
  args: {
14
14
  baseDir: cmd.positional({
15
15
  type: cmd.string,
@@ -9,7 +9,7 @@ const transformer = replaceAnyWithUnknownTransformer();
9
9
 
10
10
  const cmdDef = cmd.command({
11
11
  name: transformer.name,
12
- version: '2.0.4',
12
+ version: '2.0.5',
13
13
  args: {
14
14
  baseDir: cmd.positional({
15
15
  type: cmd.string,
@@ -9,7 +9,7 @@ const transformer = replaceRecordWithUnknownRecordTransformer();
9
9
 
10
10
  const cmdDef = cmd.command({
11
11
  name: transformer.name,
12
- version: '2.0.4',
12
+ version: '2.0.5',
13
13
  args: {
14
14
  baseDir: cmd.positional({
15
15
  type: cmd.string,
@@ -44,7 +44,7 @@ export const runTransformerCLI = async (
44
44
 
45
45
  const files = filesResult.value;
46
46
 
47
- if (files.length === 0) {
47
+ if (Arr.isArrayOfLength(files, 0)) {
48
48
  echoIfNotSilent(
49
49
  options.uncommitted
50
50
  ? 'No uncommitted files found'
@@ -71,7 +71,7 @@ export const runTransformerCLI = async (
71
71
  📊 Total: ${files.length}
72
72
  `);
73
73
 
74
- if (errorFiles.length > 0) {
74
+ if (Arr.isNonEmpty(errorFiles)) {
75
75
  echoIfNotSilent('\nFiles with errors:');
76
76
 
77
77
  for (const fileName of errorFiles) {
@@ -81,7 +81,7 @@ export const runTransformerCLI = async (
81
81
 
82
82
  echoIfNotSilent(hr);
83
83
 
84
- if (errorFiles.length > 0) {
84
+ if (Arr.isNonEmpty(errorFiles)) {
85
85
  return Result.err(undefined);
86
86
  }
87
87
 
@@ -41,19 +41,18 @@ export const convertInterfaceToTypeTransformer = (): TsMorphTransformer =>
41
41
  const members = interfaceDecl.getMembers();
42
42
 
43
43
  // Build type parameters string
44
- const typeParamsStr =
45
- typeParameters.length > 0
46
- ? `<${typeParameters.map((tp) => tp.getText()).join(', ')}>`
47
- : '';
44
+ const typeParamsStr = Arr.isNonEmpty(typeParameters)
45
+ ? `<${typeParameters.map((tp) => tp.getText()).join(', ')}>`
46
+ : '';
48
47
 
49
48
  // Build type literal from members
50
49
  const mut_typeBody: string = (() => {
51
- if (extendsExpressions.length === 0) {
50
+ if (Arr.isArrayOfLength(extendsExpressions, 0)) {
52
51
  // No extends: simple type literal
53
52
  return buildTypeLiteral(members);
54
53
  }
55
54
 
56
- if (members.length === 0) {
55
+ if (Arr.isArrayOfLength(members, 0)) {
57
56
  // Only extends, no own members: union of extended types
58
57
  const extendedTypes = extendsExpressions.map((ext) =>
59
58
  ext.getText(),
@@ -98,7 +97,7 @@ export const convertInterfaceToTypeTransformer = (): TsMorphTransformer =>
98
97
  const buildTypeLiteral = (
99
98
  members: readonly tsm.TypeElementTypes[],
100
99
  ): string => {
101
- if (members.length === 0) {
100
+ if (Arr.isArrayOfLength(members, 0)) {
102
101
  return 'Record<string, never>';
103
102
  }
104
103
 
@@ -15,10 +15,11 @@ import {
15
15
  isReadonlyTupleOrArrayTypeNode,
16
16
  isReadonlyTupleTypeNode,
17
17
  isReadonlyTypeReferenceNode,
18
- type ReadonlyTypeReferenceNode,
19
18
  removeParentheses,
19
+ shouldAvoidParenthesesForReadonly,
20
20
  unwrapReadonlyTypeArgText,
21
21
  wrapWithParentheses,
22
+ type ReadonlyTypeReferenceNode,
22
23
  } from '../functions/index.mjs';
23
24
  import { replaceNodeWithDebugPrint } from '../utils/index.mjs';
24
25
  import {
@@ -552,9 +553,13 @@ const transformTypeReferenceNode = (
552
553
  T.isKind(tsm.SyntaxKind.ArrayType) ||
553
554
  T.isKind(tsm.SyntaxKind.TupleType)
554
555
  ) {
556
+ const readonlyText = `readonly ${T.getFullText()}`;
557
+
555
558
  options.replaceNode(
556
559
  node,
557
- wrapWithParentheses(`readonly ${T.getFullText()}`),
560
+ shouldAvoidParenthesesForReadonly(node)
561
+ ? readonlyText
562
+ : wrapWithParentheses(readonlyText),
558
563
  );
559
564
 
560
565
  return;
@@ -626,9 +631,13 @@ const transformTypeReferenceNode = (
626
631
  T.isKind(tsm.SyntaxKind.ArrayType) &&
627
632
  isAtomicTypeNode(T.getElementTypeNode())
628
633
  ) {
634
+ const readonlyText = `readonly ${T.getFullText()}`;
635
+
629
636
  options.replaceNode(
630
637
  node,
631
- wrapWithParentheses(`readonly ${T.getFullText()}`),
638
+ shouldAvoidParenthesesForReadonly(node)
639
+ ? readonlyText
640
+ : wrapWithParentheses(readonlyText),
632
641
  );
633
642
 
634
643
  return;
@@ -640,9 +649,13 @@ const transformTypeReferenceNode = (
640
649
  T.isKind(tsm.SyntaxKind.TupleType) &&
641
650
  T.getElements().every(isAtomicTypeNode)
642
651
  ) {
652
+ const readonlyText = `readonly ${T.getFullText()}`;
653
+
643
654
  options.replaceNode(
644
655
  node,
645
- wrapWithParentheses(`readonly ${T.getFullText()}`),
656
+ shouldAvoidParenthesesForReadonly(node)
657
+ ? readonlyText
658
+ : wrapWithParentheses(readonlyText),
646
659
  );
647
660
 
648
661
  return;
@@ -692,9 +705,13 @@ const transformArrayTypeNode = (
692
705
  case 'Readonly':
693
706
  case 'none':
694
707
  if (readonlyContext.indexedAccessDepth === 0) {
708
+ const readonlyText = `readonly ${node.getFullText()}`;
709
+
695
710
  options.replaceNode(
696
711
  node,
697
- wrapWithParentheses(`readonly ${node.getFullText()}`),
712
+ shouldAvoidParenthesesForReadonly(node)
713
+ ? readonlyText
714
+ : wrapWithParentheses(readonlyText),
698
715
  );
699
716
  }
700
717
  }
@@ -735,9 +752,13 @@ const transformTupleTypeNode = (
735
752
  case 'Readonly':
736
753
  case 'none':
737
754
  if (readonlyContext.indexedAccessDepth === 0) {
755
+ const readonlyText = `readonly ${node.getFullText()}`;
756
+
738
757
  options.replaceNode(
739
758
  node,
740
- wrapWithParentheses(`readonly ${node.getFullText()}`),
759
+ shouldAvoidParenthesesForReadonly(node)
760
+ ? readonlyText
761
+ : wrapWithParentheses(readonlyText),
741
762
  );
742
763
  }
743
764
  }
@@ -825,7 +846,10 @@ const transformTypeLiteralNode = (
825
846
  >,
826
847
  options: ReadonlyTransformerOptionsInternal,
827
848
  ): void => {
828
- if (options.ignoreEmptyObjectTypes && node.getMembers().length === 0) {
849
+ if (
850
+ options.ignoreEmptyObjectTypes &&
851
+ Arr.isArrayOfLength(node.getMembers(), 0)
852
+ ) {
829
853
  return;
830
854
  }
831
855
 
@@ -1130,7 +1154,8 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1130
1154
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1131
1155
  n: tsm.TypeLiteralNode | ReadonlyTypeReferenceNode,
1132
1156
  ): boolean =>
1133
- n.isKind(tsm.SyntaxKind.TypeLiteral) && n.getMembers().length === 0;
1157
+ n.isKind(tsm.SyntaxKind.TypeLiteral) &&
1158
+ Arr.isArrayOfLength(n.getMembers(), 0);
1134
1159
 
1135
1160
  const nonEmptyTypeLiterals =
1136
1161
  typeLiterals === undefined
@@ -1145,7 +1170,8 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1145
1170
  : typeLiterals.nodes.filter((n) => isEmptyTypeLiteral(n));
1146
1171
 
1147
1172
  const typeLiteralsWrappedWithReadonly: readonly [] | readonly [string] =
1148
- nonEmptyTypeLiterals === undefined || nonEmptyTypeLiterals.length === 0
1173
+ nonEmptyTypeLiterals === undefined ||
1174
+ Arr.isArrayOfLength(nonEmptyTypeLiterals, 0)
1149
1175
  ? ([] as const)
1150
1176
  : ([
1151
1177
  unionToString({
@@ -1172,13 +1198,13 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1172
1198
  arraysAndTuples,
1173
1199
  (a) => [a.nodes.map((n) => n.getFullText()), a.firstPosition] as const,
1174
1200
  ),
1175
- nonEmptyTypeLiterals !== undefined && nonEmptyTypeLiterals.length > 0
1201
+ nonEmptyTypeLiterals !== undefined && Arr.isNonEmpty(nonEmptyTypeLiterals)
1176
1202
  ? mapNullable(
1177
1203
  typeLiterals,
1178
1204
  (a) => [typeLiteralsWrappedWithReadonly, a.firstPosition] as const,
1179
1205
  )
1180
1206
  : undefined,
1181
- emptyTypeLiterals !== undefined && emptyTypeLiterals.length > 0
1207
+ emptyTypeLiterals !== undefined && Arr.isNonEmpty(emptyTypeLiterals)
1182
1208
  ? mapNullable(
1183
1209
  typeLiterals,
1184
1210
  (a) =>
@@ -1187,7 +1213,7 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1187
1213
  // Use a position after non-empty type literals if they exist
1188
1214
  a.firstPosition +
1189
1215
  (nonEmptyTypeLiterals !== undefined &&
1190
- nonEmptyTypeLiterals.length > 0
1216
+ Arr.isNonEmpty(nonEmptyTypeLiterals)
1191
1217
  ? 0.5
1192
1218
  : 0),
1193
1219
  ] as const,
@@ -1226,7 +1252,7 @@ const unionToString = ({
1226
1252
  op: '&' | '|';
1227
1253
  wrapWithReadonly: boolean | string;
1228
1254
  }>): string =>
1229
- types.length === 0
1255
+ Arr.isArrayOfLength(types, 0)
1230
1256
  ? 'never'
1231
1257
  : Arr.isArrayOfLength(types, 1)
1232
1258
  ? wrapWithReadonly === false
@@ -3816,6 +3816,85 @@ describe(convertToReadonlyTransformer, () => {
3816
3816
  ])('$name', testFn);
3817
3817
  });
3818
3818
 
3819
+ describe('shouldAvoidParenthesesForReadonly', () => {
3820
+ test.each([
3821
+ {
3822
+ name: 'Type predicate with array - avoid parentheses',
3823
+ source: dedent`
3824
+ function isStringArray(x: unknown): x is string[] {
3825
+ return Array.isArray(x);
3826
+ }
3827
+ `,
3828
+ expected: dedent`
3829
+ function isStringArray(x: unknown): x is readonly string[] {
3830
+ return Array.isArray(x);
3831
+ }
3832
+ `,
3833
+ },
3834
+ {
3835
+ name: 'Type predicate with tuple - avoid parentheses',
3836
+ source: dedent`
3837
+ function isTuple(x: unknown): x is [string, number] {
3838
+ return Array.isArray(x) && x.length === 2;
3839
+ }
3840
+ `,
3841
+ expected: dedent`
3842
+ function isTuple(x: unknown): x is readonly [string, number] {
3843
+ return Array.isArray(x) && x.length === 2;
3844
+ }
3845
+ `,
3846
+ },
3847
+ {
3848
+ name: 'Type predicate with Readonly<T[]> - avoid parentheses',
3849
+ source: dedent`
3850
+ function isArray(x: unknown): x is Readonly<string[]> {
3851
+ return Array.isArray(x);
3852
+ }
3853
+ `,
3854
+ expected: dedent`
3855
+ function isArray(x: unknown): x is readonly string[] {
3856
+ return Array.isArray(x);
3857
+ }
3858
+ `,
3859
+ },
3860
+ {
3861
+ name: 'Type predicate with Readonly<[T, U]> - avoid parentheses',
3862
+ source: dedent`
3863
+ function isTuple(x: unknown): x is Readonly<[string, number]> {
3864
+ return Array.isArray(x);
3865
+ }
3866
+ `,
3867
+ expected: dedent`
3868
+ function isTuple(x: unknown): x is readonly [string, number] {
3869
+ return Array.isArray(x);
3870
+ }
3871
+ `,
3872
+ },
3873
+ {
3874
+ name: 'Non-type predicate context - keep parentheses (handled by formatter)',
3875
+ source: dedent`
3876
+ type T = Readonly<string[]>;
3877
+ `,
3878
+ expected: dedent`
3879
+ type T = readonly string[];
3880
+ `,
3881
+ },
3882
+ {
3883
+ name: 'Function return type - keep parentheses (handled by formatter)',
3884
+ source: dedent`
3885
+ function foo(): Readonly<number[]> {
3886
+ return [];
3887
+ }
3888
+ `,
3889
+ expected: dedent`
3890
+ function foo(): readonly number[] {
3891
+ return [];
3892
+ }
3893
+ `,
3894
+ },
3895
+ ])('$name', testFn);
3896
+ });
3897
+
3819
3898
  describe('Error Cases', () => {
3820
3899
  test('Invalid DeepReadonlyTypeName', () => {
3821
3900
  expect(() => {