ts-codemod-lib 4.0.1 → 4.1.0
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/README.md +2 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts +24 -3
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +204 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/package.json +9 -9
- package/src/functions/ast-transformers/convert-to-readonly-record-import.test.mts +458 -0
- package/src/functions/ast-transformers/convert-to-readonly.mts +318 -5
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +20 -0
|
@@ -49,6 +49,10 @@ import type { TsMorphTransformer } from './types.mjs';
|
|
|
49
49
|
|
|
50
50
|
const TRANSFORMER_NAME = 'convert-to-readonly';
|
|
51
51
|
|
|
52
|
+
const READONLY_RECORD_NAME = 'ReadonlyRecord';
|
|
53
|
+
|
|
54
|
+
const READONLY_RECORD_MODULE = 'ts-type-forge';
|
|
55
|
+
|
|
52
56
|
export const convertToReadonlyTransformer = (
|
|
53
57
|
options?: ReadonlyTransformerOptions,
|
|
54
58
|
): TsMorphTransformer => {
|
|
@@ -92,6 +96,7 @@ export const convertToReadonlyTransformer = (
|
|
|
92
96
|
options?.mutablePropertyPrefixes ?? DEFAULT_MUTABLE_PREFIXES,
|
|
93
97
|
),
|
|
94
98
|
recordStyle: options?.recordStyle ?? 'ReadonlyRecord',
|
|
99
|
+
importStyle: options?.importStyle ?? 'named',
|
|
95
100
|
debugPrint: options?.debug === true ? console.debug : () => {},
|
|
96
101
|
replaceNode:
|
|
97
102
|
options?.debug === true
|
|
@@ -102,8 +107,30 @@ export const convertToReadonlyTransformer = (
|
|
|
102
107
|
return {
|
|
103
108
|
name: TRANSFORMER_NAME,
|
|
104
109
|
transform: (sourceAst) => {
|
|
110
|
+
const scope =
|
|
111
|
+
optionsInternal.recordStyle === 'ReadonlyRecord' &&
|
|
112
|
+
optionsInternal.importStyle === 'named'
|
|
113
|
+
? readonlyRecordScope(sourceAst.compilerNode)
|
|
114
|
+
: 'in-scope';
|
|
115
|
+
|
|
116
|
+
// An import would turn a script into a module, and its declarations would
|
|
117
|
+
// stop being global (a `global.d.ts`); in a file whose top level declares
|
|
118
|
+
// the name as a value or a namespace, it could clash. Either gets the
|
|
119
|
+
// built-in style instead.
|
|
120
|
+
const mayImport =
|
|
121
|
+
scope === 'free' && tsm.ts.isExternalModule(sourceAst.compilerNode);
|
|
122
|
+
|
|
123
|
+
const fileOptions: ReadonlyTransformerOptionsInternal =
|
|
124
|
+
scope !== 'in-scope' && !mayImport
|
|
125
|
+
? ({ ...optionsInternal, recordStyle: 'Readonly<Record>' } as const)
|
|
126
|
+
: optionsInternal;
|
|
127
|
+
|
|
105
128
|
for (const node of sourceAst.getChildren()) {
|
|
106
|
-
transformNode(node, initialReadonlyContext,
|
|
129
|
+
transformNode(node, initialReadonlyContext, fileOptions);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (mayImport && referencesReadonlyRecord(sourceAst.compilerNode)) {
|
|
133
|
+
addReadonlyRecordImport(sourceAst);
|
|
107
134
|
}
|
|
108
135
|
},
|
|
109
136
|
};
|
|
@@ -121,7 +148,7 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
|
|
|
121
148
|
*/
|
|
122
149
|
typeName?: string;
|
|
123
150
|
|
|
124
|
-
|
|
151
|
+
/*
|
|
125
152
|
* Whether to apply `DeepReadonly` aggressively or remove `DeepReadonly` as
|
|
126
153
|
* possible.
|
|
127
154
|
*
|
|
@@ -201,9 +228,8 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
|
|
|
201
228
|
* - `"ReadonlyRecord"`: Uses the `ReadonlyRecord` type utility provided by
|
|
202
229
|
* `ts-type-forge` (as the `ts-type-forge/prefer-readonly-or-mutable-record`
|
|
203
230
|
* ESLint rule suggests). Occurrences of `Readonly<Record<K, V>>` are also
|
|
204
|
-
* unified to `ReadonlyRecord<K, V>`.
|
|
205
|
-
*
|
|
206
|
-
* (e.g. globally via `ts-type-forge`'s `global.d.mts`).
|
|
231
|
+
* unified to `ReadonlyRecord<K, V>`. How the name is brought into scope
|
|
232
|
+
* is set by `importStyle`.
|
|
207
233
|
* - `"Readonly<Record>"`: Uses only built-in utility types
|
|
208
234
|
* (`Readonly<Record<K, V>>`). Occurrences of `ReadonlyRecord<K, V>` are
|
|
209
235
|
* also unified to `Readonly<Record<K, V>>`.
|
|
@@ -215,6 +241,29 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
|
|
|
215
241
|
*/
|
|
216
242
|
recordStyle?: 'Readonly<Record>' | 'ReadonlyRecord';
|
|
217
243
|
|
|
244
|
+
/**
|
|
245
|
+
* How `ReadonlyRecord` is brought into scope with `recordStyle:
|
|
246
|
+
* "ReadonlyRecord"` (the same choice as the `importStyle` option of
|
|
247
|
+
* `eslint-plugin-ts-type-forge`).
|
|
248
|
+
*
|
|
249
|
+
* - `"named"`: When the transformer writes `ReadonlyRecord` into a file that
|
|
250
|
+
* does not mention the name yet, it also imports it — `import type {
|
|
251
|
+
* ReadonlyRecord } from 'ts-type-forge';`, merged into an existing named
|
|
252
|
+
* import of `ts-type-forge` where there is one. A file that already
|
|
253
|
+
* imports, declares or uses the name (as a global from
|
|
254
|
+
* `ts-type-forge/global`) is left to resolve it as before. A script (a
|
|
255
|
+
* file with no import or export), which an import would turn into a
|
|
256
|
+
* module, and a file whose top level declares the name as a value only,
|
|
257
|
+
* which an import could clash with, get `Readonly<Record<K, V>>` instead.
|
|
258
|
+
* - `"global"`: Imports are never touched, so `ReadonlyRecord` has to be in
|
|
259
|
+
* scope already: globally via `ts-type-forge/global`, or by an import the
|
|
260
|
+
* caller writes itself (as when transforming a file one statement at a
|
|
261
|
+
* time, where each statement would otherwise get an import of its own).
|
|
262
|
+
*
|
|
263
|
+
* @default 'named'
|
|
264
|
+
*/
|
|
265
|
+
importStyle?: 'global' | 'named';
|
|
266
|
+
|
|
218
267
|
debug?: boolean;
|
|
219
268
|
}>;
|
|
220
269
|
|
|
@@ -236,6 +285,8 @@ type ReadonlyTransformerOptionsInternal = Readonly<{
|
|
|
236
285
|
|
|
237
286
|
recordStyle: 'Readonly<Record>' | 'ReadonlyRecord';
|
|
238
287
|
|
|
288
|
+
importStyle: 'global' | 'named';
|
|
289
|
+
|
|
239
290
|
debugPrint: (...args: readonly unknown[]) => void;
|
|
240
291
|
replaceNode: (node: tsm.Node, newNodeText: string) => void;
|
|
241
292
|
}>;
|
|
@@ -501,6 +552,268 @@ const transformNode = (
|
|
|
501
552
|
}
|
|
502
553
|
};
|
|
503
554
|
|
|
555
|
+
/**
|
|
556
|
+
* The names a top-level statement binds, and whether each can name a type (an
|
|
557
|
+
* import can).
|
|
558
|
+
*/
|
|
559
|
+
const topLevelBindings = (
|
|
560
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
561
|
+
statement: tsm.ts.Statement,
|
|
562
|
+
): readonly Readonly<{ name: string; isType: boolean }>[] => {
|
|
563
|
+
if (tsm.ts.isImportDeclaration(statement)) {
|
|
564
|
+
const clause = statement.importClause;
|
|
565
|
+
|
|
566
|
+
const bindings = clause?.namedBindings;
|
|
567
|
+
|
|
568
|
+
// A namespace import cannot name a type.
|
|
569
|
+
const namespaceImport =
|
|
570
|
+
bindings !== undefined && tsm.ts.isNamespaceImport(bindings)
|
|
571
|
+
? ([{ name: bindings.name.text, isType: false }] as const)
|
|
572
|
+
: ([] as const);
|
|
573
|
+
|
|
574
|
+
const named =
|
|
575
|
+
bindings !== undefined && tsm.ts.isNamedImports(bindings)
|
|
576
|
+
? bindings.elements.map((element) => ({
|
|
577
|
+
name: element.name.text,
|
|
578
|
+
isType: true,
|
|
579
|
+
}))
|
|
580
|
+
: ([] as const);
|
|
581
|
+
|
|
582
|
+
const defaultImport =
|
|
583
|
+
clause?.name === undefined
|
|
584
|
+
? ([] as const)
|
|
585
|
+
: ([{ name: clause.name.text, isType: true }] as const);
|
|
586
|
+
|
|
587
|
+
return [...defaultImport, ...namespaceImport, ...named];
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (tsm.ts.isImportEqualsDeclaration(statement)) {
|
|
591
|
+
return [{ name: statement.name.text, isType: true }];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (
|
|
595
|
+
tsm.ts.isTypeAliasDeclaration(statement) ||
|
|
596
|
+
tsm.ts.isInterfaceDeclaration(statement) ||
|
|
597
|
+
tsm.ts.isClassDeclaration(statement) ||
|
|
598
|
+
tsm.ts.isEnumDeclaration(statement)
|
|
599
|
+
) {
|
|
600
|
+
return statement.name === undefined
|
|
601
|
+
? []
|
|
602
|
+
: [{ name: statement.name.text, isType: true }];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (
|
|
606
|
+
(tsm.ts.isFunctionDeclaration(statement) ||
|
|
607
|
+
tsm.ts.isModuleDeclaration(statement)) &&
|
|
608
|
+
statement.name !== undefined &&
|
|
609
|
+
tsm.ts.isIdentifier(statement.name)
|
|
610
|
+
) {
|
|
611
|
+
return [{ name: statement.name.text, isType: false }];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (tsm.ts.isVariableStatement(statement)) {
|
|
615
|
+
return statement.declarationList.declarations.flatMap((declaration) =>
|
|
616
|
+
tsm.ts.isIdentifier(declaration.name)
|
|
617
|
+
? [{ name: declaration.name.text, isType: false }]
|
|
618
|
+
: [],
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return [];
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Whether the file has a type reference to `ReadonlyRecord` (an `extends` or
|
|
627
|
+
* `implements` clause included).
|
|
628
|
+
*/
|
|
629
|
+
const referencesReadonlyRecord = (node: tsm.ts.Node): boolean =>
|
|
630
|
+
(tsm.ts.isTypeReferenceNode(node) &&
|
|
631
|
+
tsm.ts.isIdentifier(node.typeName) &&
|
|
632
|
+
node.typeName.text === READONLY_RECORD_NAME) ||
|
|
633
|
+
(tsm.ts.isExpressionWithTypeArguments(node) &&
|
|
634
|
+
tsm.ts.isIdentifier(node.expression) &&
|
|
635
|
+
node.expression.text === READONLY_RECORD_NAME) ||
|
|
636
|
+
(tsm.ts.forEachChild(
|
|
637
|
+
node,
|
|
638
|
+
(child) => referencesReadonlyRecord(child) || undefined,
|
|
639
|
+
) ??
|
|
640
|
+
false);
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Whether `ReadonlyRecord` can be written in the file as it is, with
|
|
644
|
+
* `importStyle: "named"`:
|
|
645
|
+
*
|
|
646
|
+
* - `"in-scope"`: the top level imports it or declares it as a type, or the
|
|
647
|
+
* file has a type reference to it, which without either relies on a global.
|
|
648
|
+
* - `"taken"`: the top level declares it as a value or a namespace only, which
|
|
649
|
+
* an import of the name could clash with (an exported one does).
|
|
650
|
+
* - `"free"`: neither; it can be imported.
|
|
651
|
+
*
|
|
652
|
+
* A qualified name (`tf.ReadonlyRecord`), a property name, the original name of
|
|
653
|
+
* an aliased import (`ReadonlyRecord as RR`) and a declaration in an inner
|
|
654
|
+
* scope bind nothing at the top level, so they do not count.
|
|
655
|
+
*/
|
|
656
|
+
const readonlyRecordScope = (
|
|
657
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
658
|
+
sourceFile: tsm.ts.SourceFile,
|
|
659
|
+
): 'free' | 'in-scope' | 'taken' => {
|
|
660
|
+
const topLevelNames = sourceFile.statements.flatMap(topLevelBindings);
|
|
661
|
+
|
|
662
|
+
return topLevelNames.some(
|
|
663
|
+
({ name, isType }) => isType && name === READONLY_RECORD_NAME,
|
|
664
|
+
) || referencesReadonlyRecord(sourceFile)
|
|
665
|
+
? 'in-scope'
|
|
666
|
+
: topLevelNames.some(({ name }) => name === READONLY_RECORD_NAME)
|
|
667
|
+
? 'taken'
|
|
668
|
+
: 'free';
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* `end`, moved past the comments that start on the same line after it: one such
|
|
673
|
+
* as `// eslint-disable-line` is about that line.
|
|
674
|
+
*/
|
|
675
|
+
const endOfLineComments = (text: string, end: number): number =>
|
|
676
|
+
tsm.ts.getTrailingCommentRanges(text, end)?.at(-1)?.end ?? end;
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Where a new import goes in a file with no import and no directive prologue:
|
|
680
|
+
* before the first statement, and before the run of JSDoc and `//` comments
|
|
681
|
+
* right above it with no blank line in between (its documentation, an
|
|
682
|
+
* `eslint-disable-next-line`), which belong to it. A comment separated from it
|
|
683
|
+
* by a blank line, a `/* ... *\/` comment (a file-wide `eslint-disable`, a
|
|
684
|
+
* license), a `///` directive, a pragma (`@ts-nocheck`, `@jsxImportSource`),
|
|
685
|
+
* which TypeScript reads only above the first statement, and a shebang stay
|
|
686
|
+
* above the import.
|
|
687
|
+
*/
|
|
688
|
+
const newImportPosition = (
|
|
689
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
690
|
+
sourceFile: tsm.SourceFile,
|
|
691
|
+
): number => {
|
|
692
|
+
const text = sourceFile.getFullText();
|
|
693
|
+
|
|
694
|
+
const first = sourceFile.getStatements().at(0);
|
|
695
|
+
|
|
696
|
+
if (first === undefined) {
|
|
697
|
+
return text.length;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const comments =
|
|
701
|
+
tsm.ts.getLeadingCommentRanges(text, first.getFullStart()) ?? [];
|
|
702
|
+
|
|
703
|
+
let mut_position = first.getStart();
|
|
704
|
+
|
|
705
|
+
for (const comment of comments.toReversed()) {
|
|
706
|
+
const commentText = text.slice(comment.pos, comment.end);
|
|
707
|
+
|
|
708
|
+
if (
|
|
709
|
+
/\n[^\S\n]*\n/u.test(text.slice(comment.end, mut_position)) ||
|
|
710
|
+
commentText.startsWith('///') ||
|
|
711
|
+
(commentText.startsWith('/*') && !commentText.startsWith('/**')) ||
|
|
712
|
+
/@(?:ts-nocheck|ts-check|jsx)/u.test(commentText)
|
|
713
|
+
) {
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
mut_position = comment.pos;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return mut_position;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
/** The leading `'use client';`-like statements of the file. */
|
|
724
|
+
const directivePrologue = (
|
|
725
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
726
|
+
sourceFile: tsm.SourceFile,
|
|
727
|
+
): readonly tsm.ts.Statement[] => {
|
|
728
|
+
const statements = sourceFile.compilerNode.statements;
|
|
729
|
+
|
|
730
|
+
const end = statements.findIndex(
|
|
731
|
+
(statement) =>
|
|
732
|
+
!(
|
|
733
|
+
tsm.ts.isExpressionStatement(statement) &&
|
|
734
|
+
tsm.ts.isStringLiteral(statement.expression)
|
|
735
|
+
),
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
return statements.slice(0, end === -1 ? statements.length : end);
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const insertReadonlyRecordImport = (
|
|
742
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
743
|
+
sourceFile: tsm.SourceFile,
|
|
744
|
+
text: string,
|
|
745
|
+
): void => {
|
|
746
|
+
const imports = sourceFile.getImportDeclarations();
|
|
747
|
+
|
|
748
|
+
const hosts = imports.filter(
|
|
749
|
+
(decl) =>
|
|
750
|
+
decl.getModuleSpecifierValue() === READONLY_RECORD_MODULE &&
|
|
751
|
+
tsm.Node.isNamedImports(decl.getImportClause()?.getNamedBindings()),
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
const host = hosts.find((decl) => decl.isTypeOnly()) ?? hosts.at(0);
|
|
755
|
+
|
|
756
|
+
if (host !== undefined) {
|
|
757
|
+
// `import type { ..., type X }` is an error: the declaration already makes
|
|
758
|
+
// every name type-only.
|
|
759
|
+
host.addNamedImport({
|
|
760
|
+
name: READONLY_RECORD_NAME,
|
|
761
|
+
isTypeOnly: !host.isTypeOnly(),
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// `import type`, not `import { type X }`, which `verbatimModuleSyntax` keeps
|
|
768
|
+
// as `import {} from 'ts-type-forge'`, a module with nothing to run.
|
|
769
|
+
const declaration =
|
|
770
|
+
`import type { ${READONLY_RECORD_NAME} } from '${READONLY_RECORD_MODULE}';` as const;
|
|
771
|
+
|
|
772
|
+
const anchorEnd =
|
|
773
|
+
imports.at(-1)?.getEnd() ?? directivePrologue(sourceFile).at(-1)?.end;
|
|
774
|
+
|
|
775
|
+
if (anchorEnd === undefined) {
|
|
776
|
+
sourceFile.insertText(newImportPosition(sourceFile), `${declaration}\n\n`);
|
|
777
|
+
} else {
|
|
778
|
+
sourceFile.insertText(
|
|
779
|
+
endOfLineComments(text, anchorEnd),
|
|
780
|
+
`\n${declaration}`,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Imports `ReadonlyRecord` from `ts-type-forge`: into the braces of an existing
|
|
787
|
+
* import of the module where there is one, or else as a new declaration —
|
|
788
|
+
* after the last import, else after the directive prologue, else before the
|
|
789
|
+
* first statement and the comments attached to it (see
|
|
790
|
+
* {@link newImportPosition}). The line breaks follow the file's.
|
|
791
|
+
*/
|
|
792
|
+
const addReadonlyRecordImport = (
|
|
793
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
794
|
+
sourceFile: tsm.SourceFile,
|
|
795
|
+
): void => {
|
|
796
|
+
const text = sourceFile.getFullText();
|
|
797
|
+
|
|
798
|
+
// ts-morph writes its line breaks as the project's setting says, so the
|
|
799
|
+
// setting follows the file's for the edit.
|
|
800
|
+
const { manipulationSettings } = sourceFile.getProject();
|
|
801
|
+
|
|
802
|
+
const newLineKind = manipulationSettings.getNewLineKind();
|
|
803
|
+
|
|
804
|
+
manipulationSettings.set({
|
|
805
|
+
newLineKind: text.includes('\r\n')
|
|
806
|
+
? tsm.NewLineKind.CarriageReturnLineFeed
|
|
807
|
+
: tsm.NewLineKind.LineFeed,
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
try {
|
|
811
|
+
insertReadonlyRecordImport(sourceFile, text);
|
|
812
|
+
} finally {
|
|
813
|
+
manipulationSettings.set({ newLineKind });
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
|
|
504
817
|
//
|
|
505
818
|
// Transformer implementation for each node type
|
|
506
819
|
//
|
|
@@ -215,6 +215,13 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
215
215
|
|
|
216
216
|
describe('Records', () => {
|
|
217
217
|
describe('recordStyle: "ReadonlyRecord" (default)', () => {
|
|
218
|
+
// These sources are scripts, which get `Readonly<Record<K, V>>` unless
|
|
219
|
+
// the name is in scope; how it is brought into scope is tested in
|
|
220
|
+
// convert-to-readonly-record-import.test.mts.
|
|
221
|
+
const options = {
|
|
222
|
+
importStyle: 'global',
|
|
223
|
+
} as const satisfies ReadonlyTransformerOptions;
|
|
224
|
+
|
|
218
225
|
test.each([
|
|
219
226
|
{
|
|
220
227
|
name: 'Type alias',
|
|
@@ -224,6 +231,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
224
231
|
expected: dedent`
|
|
225
232
|
type Foo = ReadonlyRecord<string, number>
|
|
226
233
|
`,
|
|
234
|
+
options,
|
|
227
235
|
},
|
|
228
236
|
{
|
|
229
237
|
name: 'ReadonlyRecord<K, V> is unchanged',
|
|
@@ -233,6 +241,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
233
241
|
expected: dedent`
|
|
234
242
|
type Foo = ReadonlyRecord<string, number>
|
|
235
243
|
`,
|
|
244
|
+
options,
|
|
236
245
|
},
|
|
237
246
|
{
|
|
238
247
|
name: 'Readonly<Record<K, V>> is unified to ReadonlyRecord<K, V>',
|
|
@@ -242,6 +251,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
242
251
|
expected: dedent`
|
|
243
252
|
type Foo = ReadonlyRecord<string, number>
|
|
244
253
|
`,
|
|
254
|
+
options,
|
|
245
255
|
},
|
|
246
256
|
{
|
|
247
257
|
name: 'Redundant Readonly<ReadonlyRecord<K, V>> is normalized',
|
|
@@ -251,6 +261,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
251
261
|
expected: dedent`
|
|
252
262
|
type Foo = ReadonlyRecord<string, number>
|
|
253
263
|
`,
|
|
264
|
+
options,
|
|
254
265
|
},
|
|
255
266
|
{
|
|
256
267
|
name: 'Value type is converted recursively',
|
|
@@ -260,6 +271,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
260
271
|
expected: dedent`
|
|
261
272
|
type Foo = ReadonlyRecord<string, readonly number[]>
|
|
262
273
|
`,
|
|
274
|
+
options,
|
|
263
275
|
},
|
|
264
276
|
{
|
|
265
277
|
name: 'Value type inside Readonly<Record<K, V>> is converted recursively',
|
|
@@ -269,6 +281,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
269
281
|
expected: dedent`
|
|
270
282
|
type Foo = ReadonlyRecord<string, readonly number[]>
|
|
271
283
|
`,
|
|
284
|
+
options,
|
|
272
285
|
},
|
|
273
286
|
{
|
|
274
287
|
name: 'Value type inside ReadonlyRecord<K, V> is converted recursively',
|
|
@@ -278,6 +291,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
278
291
|
expected: dedent`
|
|
279
292
|
type Foo = ReadonlyRecord<string, readonly number[]>
|
|
280
293
|
`,
|
|
294
|
+
options,
|
|
281
295
|
},
|
|
282
296
|
{
|
|
283
297
|
name: 'In type literal member',
|
|
@@ -287,6 +301,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
287
301
|
expected: dedent`
|
|
288
302
|
type Foo = Readonly<{ a: ReadonlyRecord<string, number> }>
|
|
289
303
|
`,
|
|
304
|
+
options,
|
|
290
305
|
},
|
|
291
306
|
{
|
|
292
307
|
name: 'In function args',
|
|
@@ -296,6 +311,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
296
311
|
expected: dedent`
|
|
297
312
|
function foo(a: ReadonlyRecord<string, number>, b: Promise<ReadonlyRecord<string, number>>) {}
|
|
298
313
|
`,
|
|
314
|
+
options,
|
|
299
315
|
},
|
|
300
316
|
{
|
|
301
317
|
name: 'DeepReadonly<Record<K, V>> is unchanged',
|
|
@@ -305,6 +321,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
305
321
|
expected: dedent`
|
|
306
322
|
type Foo = DeepReadonly<Record<string, number[]>>
|
|
307
323
|
`,
|
|
324
|
+
options,
|
|
308
325
|
},
|
|
309
326
|
{
|
|
310
327
|
name: 'DeepReadonly<ReadonlyRecord<K, V>> is unchanged',
|
|
@@ -314,6 +331,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
314
331
|
expected: dedent`
|
|
315
332
|
type Foo = DeepReadonly<ReadonlyRecord<string, number>>
|
|
316
333
|
`,
|
|
334
|
+
options,
|
|
317
335
|
},
|
|
318
336
|
{
|
|
319
337
|
name: 'Record<K, V> under indexed access is kept as is',
|
|
@@ -323,6 +341,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
323
341
|
expected: dedent`
|
|
324
342
|
type Foo = Record<string, number>[string]
|
|
325
343
|
`,
|
|
344
|
+
options,
|
|
326
345
|
},
|
|
327
346
|
{
|
|
328
347
|
name: 'ReadonlyRecord<K, V> under indexed access is unchanged',
|
|
@@ -332,6 +351,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
332
351
|
expected: dedent`
|
|
333
352
|
type Foo = ReadonlyRecord<string, number>[string]
|
|
334
353
|
`,
|
|
354
|
+
options,
|
|
335
355
|
},
|
|
336
356
|
])('$name', testFn);
|
|
337
357
|
});
|