tstyche 3.0.0 → 3.1.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/README.md CHANGED
@@ -57,17 +57,17 @@ test("handles numbers", () => {
57
57
  Here is the list of all matchers:
58
58
 
59
59
  - `.toBe()`, `.toBeAssignableTo()`, `.toBeAssignableWith()` compare types or types of expression,
60
- - `.toAcceptProps()` checks types of JSX component's props,
60
+ - `.toAcceptProps()` checks JSX component props type,
61
61
  - `.toHaveProperty()` looks up keys on an object type,
62
62
  - `.toRaiseError()` captures the type error message or code,
63
63
  - `.toBeString()`, `.toBeNumber()`, `.toBeVoid()` and 9 more shorthand checks for primitive types.
64
64
 
65
65
  ## Runner
66
66
 
67
- The `tstyche` command is the heart of TSTyche. For example, it can select test files by path, filter tests by name and pass them through TypeScript `4.8` and `latest`:
67
+ The `tstyche` command is the heart of TSTyche. For example, it can select test files by path, filter tests by name and pass them through a range of TypeScript versions:
68
68
 
69
- ```sh
70
- tstyche JsonObject --only external --target 4.8,latest
69
+ ```shell
70
+ tstyche query-params --only multiple --target '>=5.0 <5.3'
71
71
  ```
72
72
 
73
73
  This simple! (And it has watch mode too.)
@@ -107,13 +107,13 @@ interface MatcherNode extends ts.CallExpression {
107
107
  }
108
108
  declare class Assertion extends TestMember {
109
109
  isNot: boolean;
110
+ matcherName: ts.MemberName;
110
111
  matcherNode: MatcherNode;
111
112
  modifierNode: ts.PropertyAccessExpression;
112
113
  notNode: ts.PropertyAccessExpression | undefined;
114
+ source: ts.NodeArray<ts.Expression> | ts.NodeArray<ts.TypeNode>;
115
+ target: ts.NodeArray<ts.Expression> | ts.NodeArray<ts.TypeNode>;
113
116
  constructor(compiler: typeof ts, brand: TestMemberBrand, node: ts.CallExpression, parent: TestTree | TestMember, flags: TestMemberFlags, matcherNode: MatcherNode, modifierNode: ts.PropertyAccessExpression, notNode?: ts.PropertyAccessExpression);
114
- get matcherName(): ts.MemberName;
115
- get source(): ts.NodeArray<ts.Expression> | ts.NodeArray<ts.TypeNode>;
116
- get target(): ts.NodeArray<ts.Expression> | ts.NodeArray<ts.TypeNode>;
117
117
  }
118
118
 
119
119
  declare class CollectService {
@@ -135,12 +135,15 @@ declare class ConfigDiagnosticText {
135
135
  static expectsListItemType(optionName: string, optionBrand: OptionBrand): string;
136
136
  static expectsValue(optionName: string): string;
137
137
  static fileDoesNotExist(filePath: string): string;
138
+ static inspectSupportedVersions(): string;
138
139
  static moduleWasNotFound(specifier: string): string;
140
+ static rangeIsNotValid(value: string): string;
141
+ static rangeUsage(): Array<string>;
142
+ static requiresValueType(optionName: string, optionBrand: OptionBrand): string;
139
143
  static seen(element: string): string;
140
144
  static testFileMatchCannotStartWith(segment: string): Array<string>;
141
- static requiresValueType(optionName: string, optionBrand: OptionBrand): string;
142
145
  static unknownOption(optionName: string): string;
143
- static usage(optionName: string, optionBrand: OptionBrand): Promise<Array<string>>;
146
+ static usage(optionName: string, optionBrand: OptionBrand): Array<string>;
144
147
  static versionIsNotSupported(value: string): string;
145
148
  static watchCannotBeEnabled(): string;
146
149
  }
@@ -157,6 +160,14 @@ interface ConfigFileOptions {
157
160
  * The list of plugins to use.
158
161
  */
159
162
  plugins?: Array<string>;
163
+ /**
164
+ * Reject the 'any' type passed as an argument to the 'expect()' function or a matcher.
165
+ */
166
+ rejectAnyType?: boolean;
167
+ /**
168
+ * Reject the 'never' type passed as an argument to the 'expect()' function or a matcher.
169
+ */
170
+ rejectNeverType?: boolean;
160
171
  /**
161
172
  * The list of reporters to use.
162
173
  */
@@ -199,6 +210,10 @@ interface CommandLineOptions {
199
210
  * Install specified versions of the 'typescript' package and exit.
200
211
  */
201
212
  install?: boolean;
213
+ /**
214
+ * Print the list of supported versions of the 'typescript' package and exit.
215
+ */
216
+ list?: boolean;
202
217
  /**
203
218
  * Print the list of the selected test files and exit.
204
219
  */
@@ -296,7 +311,6 @@ interface PrimitiveTypeOptionDefinition extends BaseOptionDefinition {
296
311
  interface ItemDefinition {
297
312
  brand: OptionBrand.String;
298
313
  name: string;
299
- pattern?: string;
300
314
  }
301
315
  interface ListTypeOptionDefinition extends BaseOptionDefinition {
302
316
  brand: OptionBrand.List;
@@ -588,7 +602,7 @@ declare class ExpectService {
588
602
  private toHaveProperty;
589
603
  private toMatch;
590
604
  private toRaiseError;
591
- constructor(compiler: typeof ts, typeChecker: TypeChecker);
605
+ constructor(compiler: typeof ts, typeChecker: TypeChecker, resolvedConfig?: ResolvedConfig);
592
606
  match(assertion: Assertion, onDiagnostics: DiagnosticsHandler<Diagnostic | Array<Diagnostic>>): MatchResult | undefined;
593
607
  }
594
608
 
@@ -779,8 +793,41 @@ declare class SelectDiagnosticText {
779
793
  static noTestFilesWereSelected(resolvedConfig: ResolvedConfig): Array<string>;
780
794
  }
781
795
 
796
+ interface ManifestData {
797
+ $version?: string;
798
+ lastUpdated?: number;
799
+ npmRegistry: string;
800
+ packages: Record<string, {
801
+ integrity: string;
802
+ tarball: string;
803
+ }>;
804
+ resolutions: Record<string, string>;
805
+ versions: Array<string>;
806
+ }
807
+ declare class Manifest {
808
+ #private;
809
+ $version: string;
810
+ lastUpdated: number;
811
+ npmRegistry: string;
812
+ packages: Record<string, {
813
+ integrity: string;
814
+ tarball: string;
815
+ }>;
816
+ resolutions: Record<string, string>;
817
+ versions: Array<string>;
818
+ constructor(data: ManifestData);
819
+ isOutdated(options?: {
820
+ ageTolerance?: number;
821
+ }): boolean;
822
+ static parse(text: string): Manifest | undefined;
823
+ resolve(tag: string): string | undefined;
824
+ stringify(): string;
825
+ }
826
+
782
827
  declare class Store {
783
828
  #private;
829
+ static manifest: Manifest | undefined;
830
+ /** @deprecated Use 'Store.manifest' directly. */
784
831
  static getSupportedTags(): Promise<Array<string> | undefined>;
785
832
  static install(tag: string): Promise<void>;
786
833
  static load(tag: string): Promise<typeof ts | undefined>;
@@ -794,6 +841,7 @@ declare class Version {
794
841
  #private;
795
842
  static isGreaterThan(source: string, target: string): boolean;
796
843
  static isSatisfiedWith(source: string, target: string): boolean;
844
+ /** @deprecated Name of this method is misleading and it is also not needed. */
797
845
  static isVersionTag(target: string): boolean;
798
846
  }
799
847
 
package/build/tstyche.js CHANGED
@@ -1,13 +1,80 @@
1
+ import { writeFileSync, rmSync, existsSync, watch } from 'node:fs';
1
2
  import fs from 'node:fs/promises';
2
- import { createRequire } from 'node:module';
3
+ import path from 'node:path';
3
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
4
- import vm from 'node:vm';
5
5
  import os from 'node:os';
6
6
  import process from 'node:process';
7
- import path from 'node:path';
8
- import { writeFileSync, rmSync, existsSync, watch } from 'node:fs';
7
+ import { createRequire } from 'node:module';
8
+ import vm from 'node:vm';
9
9
  import streamConsumers from 'node:stream/consumers';
10
10
 
11
+ class ConfigDiagnosticText {
12
+ static expected(element) {
13
+ return `Expected ${element}.`;
14
+ }
15
+ static expectsListItemType(optionName, optionBrand) {
16
+ return `Item of the '${optionName}' list must be of type ${optionBrand}.`;
17
+ }
18
+ static expectsValue(optionName) {
19
+ return `Option '${optionName}' expects a value.`;
20
+ }
21
+ static fileDoesNotExist(filePath) {
22
+ return `The specified path '${filePath}' does not exist.`;
23
+ }
24
+ static inspectSupportedVersions() {
25
+ return "Use the '--list' command line option to inspect the list of supported versions.";
26
+ }
27
+ static moduleWasNotFound(specifier) {
28
+ return `The specified module '${specifier}' was not found.`;
29
+ }
30
+ static rangeIsNotValid(value) {
31
+ return `The specified range '${value}' is not valid.`;
32
+ }
33
+ static rangeUsage() {
34
+ return [
35
+ "A range must be specified using an operator and a minor version.",
36
+ "To set an upper bound, the intersection of two ranges can be used.",
37
+ "Examples: '>=5.5', '>=5.0 <5.3'.",
38
+ ];
39
+ }
40
+ static requiresValueType(optionName, optionBrand) {
41
+ return `Option '${optionName}' requires a value of type ${optionBrand}.`;
42
+ }
43
+ static seen(element) {
44
+ return `The ${element} was seen here.`;
45
+ }
46
+ static testFileMatchCannotStartWith(segment) {
47
+ return [
48
+ `A test file match pattern cannot start with '${segment}'.`,
49
+ "The test files are only collected within the 'rootPath' directory.",
50
+ ];
51
+ }
52
+ static unknownOption(optionName) {
53
+ return `Unknown option '${optionName}'.`;
54
+ }
55
+ static usage(optionName, optionBrand) {
56
+ switch (optionName.startsWith("--") ? optionName.slice(2) : optionName) {
57
+ case "target": {
58
+ const text = [];
59
+ if (optionName.startsWith("--")) {
60
+ text.push("Value for the '--target' option must be a string or a comma separated list.", "Examples: '--target 5.2', '--target next', '--target '>=5.0 <5.3, 5.4.2, >=5.5''.");
61
+ }
62
+ return text;
63
+ }
64
+ }
65
+ return [ConfigDiagnosticText.requiresValueType(optionName, optionBrand)];
66
+ }
67
+ static versionIsNotSupported(value) {
68
+ if (value === "current") {
69
+ return "Cannot use 'current' as a target. Failed to resolve the installed TypeScript module.";
70
+ }
71
+ return `TypeScript version '${value}' is not supported.`;
72
+ }
73
+ static watchCannotBeEnabled() {
74
+ return "Watch mode cannot be enabled in continuous integration environment.";
75
+ }
76
+ }
77
+
11
78
  class DiagnosticOrigin {
12
79
  assertion;
13
80
  end;
@@ -117,6 +184,49 @@ class SourceFile {
117
184
  }
118
185
  }
119
186
 
187
+ class EventEmitter {
188
+ static instanceCount = 0;
189
+ static #handlers = new Map();
190
+ static #reporters = new Map();
191
+ #scope;
192
+ constructor() {
193
+ this.#scope = EventEmitter.instanceCount++;
194
+ EventEmitter.#handlers.set(this.#scope, new Set());
195
+ EventEmitter.#reporters.set(this.#scope, new Set());
196
+ }
197
+ addHandler(handler) {
198
+ EventEmitter.#handlers.get(this.#scope)?.add(handler);
199
+ }
200
+ addReporter(reporter) {
201
+ EventEmitter.#reporters.get(this.#scope)?.add(reporter);
202
+ }
203
+ static dispatch(event) {
204
+ function forEachHandler(handlers, event) {
205
+ for (const handler of handlers) {
206
+ handler.on(event);
207
+ }
208
+ }
209
+ for (const handlers of EventEmitter.#handlers.values()) {
210
+ forEachHandler(handlers, event);
211
+ }
212
+ for (const handlers of EventEmitter.#reporters.values()) {
213
+ forEachHandler(handlers, event);
214
+ }
215
+ }
216
+ removeHandler(handler) {
217
+ EventEmitter.#handlers.get(this.#scope)?.delete(handler);
218
+ }
219
+ removeReporter(reporter) {
220
+ EventEmitter.#reporters.get(this.#scope)?.delete(reporter);
221
+ }
222
+ removeHandlers() {
223
+ EventEmitter.#handlers.get(this.#scope)?.clear();
224
+ }
225
+ removeReporters() {
226
+ EventEmitter.#reporters.get(this.#scope)?.clear();
227
+ }
228
+ }
229
+
120
230
  class Path {
121
231
  static normalizeSlashes;
122
232
  static {
@@ -222,49 +332,6 @@ class Environment {
222
332
 
223
333
  const environmentOptions = Environment.resolve();
224
334
 
225
- class EventEmitter {
226
- static instanceCount = 0;
227
- static #handlers = new Map();
228
- static #reporters = new Map();
229
- #scope;
230
- constructor() {
231
- this.#scope = EventEmitter.instanceCount++;
232
- EventEmitter.#handlers.set(this.#scope, new Set());
233
- EventEmitter.#reporters.set(this.#scope, new Set());
234
- }
235
- addHandler(handler) {
236
- EventEmitter.#handlers.get(this.#scope)?.add(handler);
237
- }
238
- addReporter(reporter) {
239
- EventEmitter.#reporters.get(this.#scope)?.add(reporter);
240
- }
241
- static dispatch(event) {
242
- function forEachHandler(handlers, event) {
243
- for (const handler of handlers) {
244
- handler.on(event);
245
- }
246
- }
247
- for (const handlers of EventEmitter.#handlers.values()) {
248
- forEachHandler(handlers, event);
249
- }
250
- for (const handlers of EventEmitter.#reporters.values()) {
251
- forEachHandler(handlers, event);
252
- }
253
- }
254
- removeHandler(handler) {
255
- EventEmitter.#handlers.get(this.#scope)?.delete(handler);
256
- }
257
- removeReporter(reporter) {
258
- EventEmitter.#reporters.get(this.#scope)?.delete(reporter);
259
- }
260
- removeHandlers() {
261
- EventEmitter.#handlers.get(this.#scope)?.clear();
262
- }
263
- removeReporters() {
264
- EventEmitter.#reporters.get(this.#scope)?.clear();
265
- }
266
- }
267
-
268
335
  class Version {
269
336
  static isGreaterThan(source, target) {
270
337
  return !(source === target) && Version.#satisfies(source, target);
@@ -465,6 +532,7 @@ class ManifestService {
465
532
  #manifestFilePath;
466
533
  #npmRegistry;
467
534
  #storePath;
535
+ #supportedVersionRegex = /^(4|5)\.\d\.\d$/;
468
536
  constructor(storePath, npmRegistry, fetcher) {
469
537
  this.#storePath = storePath;
470
538
  this.#npmRegistry = npmRegistry;
@@ -494,12 +562,12 @@ class ManifestService {
494
562
  const versions = [];
495
563
  const packageMetadata = (await response.json());
496
564
  for (const [tag, meta] of Object.entries(packageMetadata.versions)) {
497
- if (/^(4|5)\.\d\.\d$/.test(tag)) {
565
+ if (this.#supportedVersionRegex.test(tag)) {
498
566
  versions.push(tag);
499
567
  packages[tag] = { integrity: meta.dist.integrity, tarball: meta.dist.tarball };
500
568
  }
501
569
  }
502
- const minorVersions = [...new Set(versions.map((version) => version.slice(0, -2)))];
570
+ const minorVersions = new Set(versions.map((version) => version.slice(0, -2)));
503
571
  for (const tag of minorVersions) {
504
572
  const resolvedVersion = versions.findLast((version) => version.startsWith(tag));
505
573
  if (resolvedVersion != null) {
@@ -528,7 +596,7 @@ class ManifestService {
528
596
  await this.prune();
529
597
  return this.#create();
530
598
  }
531
- if (manifest.isOutdated() || options?.refresh === true) {
599
+ if (manifest.isOutdated() || options?.refresh) {
532
600
  const freshManifest = await this.#load({ suppressErrors: !options?.refresh });
533
601
  if (freshManifest != null) {
534
602
  await this.#persist(freshManifest);
@@ -637,7 +705,7 @@ class Store {
637
705
  static #compilerInstanceCache = new Map();
638
706
  static #fetcher;
639
707
  static #lockService;
640
- static #manifest;
708
+ static manifest;
641
709
  static #manifestService;
642
710
  static #packageService;
643
711
  static #npmRegistry = environmentOptions.npmRegistry;
@@ -659,12 +727,12 @@ class Store {
659
727
  return;
660
728
  }
661
729
  await Store.open();
662
- const version = Store.#manifest?.resolve(tag);
730
+ const version = Store.manifest?.resolve(tag);
663
731
  if (!version) {
664
732
  Store.#onDiagnostics(Diagnostic.error(StoreDiagnosticText.cannotAddTypeScriptPackage(tag)));
665
733
  return;
666
734
  }
667
- await Store.#packageService.ensure(version, Store.#manifest);
735
+ await Store.#packageService.ensure(version, Store.manifest);
668
736
  }
669
737
  static async load(tag) {
670
738
  let compilerInstance = Store.#compilerInstanceCache.get(tag);
@@ -677,7 +745,7 @@ class Store {
677
745
  }
678
746
  else {
679
747
  await Store.open();
680
- const version = Store.#manifest?.resolve(tag);
748
+ const version = Store.manifest?.resolve(tag);
681
749
  if (!version) {
682
750
  Store.#onDiagnostics(Diagnostic.error(StoreDiagnosticText.cannotAddTypeScriptPackage(tag)));
683
751
  return;
@@ -686,7 +754,7 @@ class Store {
686
754
  if (compilerInstance != null) {
687
755
  return compilerInstance;
688
756
  }
689
- const packagePath = await Store.#packageService.ensure(version, Store.#manifest);
757
+ const packagePath = await Store.#packageService.ensure(version, Store.manifest);
690
758
  if (packagePath != null) {
691
759
  modulePath = Path.join(packagePath, "lib", "typescript.js");
692
760
  }
@@ -725,13 +793,9 @@ class Store {
725
793
  }
726
794
  static async open() {
727
795
  Store.open = () => Promise.resolve();
728
- Store.#manifest = await Store.#manifestService.open();
729
- if (Store.#manifest != null) {
730
- Store.#supportedTags = [
731
- ...Object.keys(Store.#manifest.resolutions),
732
- ...Store.#manifest.versions,
733
- "current",
734
- ].sort();
796
+ Store.manifest = await Store.#manifestService.open();
797
+ if (Store.manifest != null) {
798
+ Store.#supportedTags = [...Object.keys(Store.manifest.resolutions), ...Store.manifest.versions, "current"].sort();
735
799
  }
736
800
  }
737
801
  static async prune() {
@@ -745,10 +809,10 @@ class Store {
745
809
  return environmentOptions.typescriptModule != null;
746
810
  }
747
811
  await Store.open();
748
- if (Store.#manifest?.isOutdated({ ageTolerance: 60 }) &&
749
- (!Version.isVersionTag(tag) ||
750
- (Store.#manifest.resolutions["latest"] != null &&
751
- Version.isGreaterThan(tag, Store.#manifest.resolutions["latest"])))) {
812
+ if (Store.manifest?.isOutdated({ ageTolerance: 60 }) &&
813
+ (!/^\d/.test(tag) ||
814
+ (Store.manifest.resolutions["latest"] != null &&
815
+ Version.isGreaterThan(tag, Store.manifest.resolutions["latest"])))) {
752
816
  Store.#onDiagnostics(Diagnostic.warning([
753
817
  StoreDiagnosticText.failedToUpdateMetadata(Store.#npmRegistry),
754
818
  StoreDiagnosticText.maybeOutdatedResolution(tag),
@@ -758,64 +822,42 @@ class Store {
758
822
  }
759
823
  }
760
824
 
761
- class ConfigDiagnosticText {
762
- static expected(element) {
763
- return `Expected ${element}.`;
764
- }
765
- static expectsListItemType(optionName, optionBrand) {
766
- return `Item of the '${optionName}' list must be of type ${optionBrand}.`;
767
- }
768
- static expectsValue(optionName) {
769
- return `Option '${optionName}' expects a value.`;
770
- }
771
- static fileDoesNotExist(filePath) {
772
- return `The specified path '${filePath}' does not exist.`;
773
- }
774
- static moduleWasNotFound(specifier) {
775
- return `The specified module '${specifier}' was not found.`;
776
- }
777
- static seen(element) {
778
- return `The ${element} was seen here.`;
779
- }
780
- static testFileMatchCannotStartWith(segment) {
781
- return [
782
- `A test file match pattern cannot start with '${segment}'.`,
783
- "The test files are only collected within the 'rootPath' directory.",
784
- ];
785
- }
786
- static requiresValueType(optionName, optionBrand) {
787
- return `Option '${optionName}' requires a value of type ${optionBrand}.`;
788
- }
789
- static unknownOption(optionName) {
790
- return `Unknown option '${optionName}'.`;
791
- }
792
- static async usage(optionName, optionBrand) {
793
- switch (optionName.startsWith("--") ? optionName.slice(2) : optionName) {
794
- case "target": {
795
- const text = [];
796
- if (optionName.startsWith("--")) {
797
- text.push("Value for the '--target' option must be a single tag or a comma separated list.", "Usage examples: '--target 4.9', '--target latest', '--target 4.9,5.3.2,current'.");
798
- }
799
- else {
800
- text.push("Item of the 'target' list must be a supported version tag.");
801
- }
802
- const supportedTags = await Store.getSupportedTags();
803
- if (supportedTags != null) {
804
- text.push(`Supported tags: ${["'", supportedTags.join("', '"), "'"].join("")}.`);
825
+ class Target {
826
+ static #rangeRegex = /^[<>]=?\d\.\d( [<>]=?\d\.\d)?$/;
827
+ static async expand(queries) {
828
+ const include = [];
829
+ for (const query of queries) {
830
+ if (!Target.isRange(query)) {
831
+ include.push(query);
832
+ continue;
833
+ }
834
+ await Store.open();
835
+ if (Store.manifest != null) {
836
+ let versions = Object.keys(Store.manifest.resolutions).slice(0, -4);
837
+ for (const comparator of query.split(" ")) {
838
+ versions = Target.#filter(comparator, versions);
805
839
  }
806
- return text;
840
+ include.push(...versions);
807
841
  }
808
842
  }
809
- return [ConfigDiagnosticText.requiresValueType(optionName, optionBrand)];
843
+ return include;
810
844
  }
811
- static versionIsNotSupported(value) {
812
- if (value === "current") {
813
- return "Cannot use 'current' as a target. Failed to resolve the installed TypeScript module.";
845
+ static #filter(comparator, versions) {
846
+ const targetVersion = comparator.replace(/^[<>]=?/, "");
847
+ switch (comparator.charAt(0)) {
848
+ case ">":
849
+ return versions.filter((sourceVersion) => comparator.charAt(1) === "="
850
+ ? Version.isSatisfiedWith(sourceVersion, targetVersion)
851
+ : Version.isGreaterThan(sourceVersion, targetVersion));
852
+ case "<":
853
+ return versions.filter((sourceVersion) => comparator.charAt(1) === "="
854
+ ? Version.isSatisfiedWith(targetVersion, sourceVersion)
855
+ : Version.isGreaterThan(targetVersion, sourceVersion));
814
856
  }
815
- return `TypeScript version '${value}' is not supported.`;
857
+ return [];
816
858
  }
817
- static watchCannotBeEnabled() {
818
- return "Watch mode cannot be enabled in continuous integration environment.";
859
+ static isRange(query) {
860
+ return Target.#rangeRegex.test(query);
819
861
  }
820
862
  }
821
863
 
@@ -851,6 +893,12 @@ class Options {
851
893
  group: 2,
852
894
  name: "install",
853
895
  },
896
+ {
897
+ brand: "bareTrue",
898
+ description: "Print the list of supported versions of the 'typescript' package and exit.",
899
+ group: 2,
900
+ name: "list",
901
+ },
854
902
  {
855
903
  brand: "bareTrue",
856
904
  description: "Print the list of the selected test files and exit.",
@@ -879,6 +927,18 @@ class Options {
879
927
  group: 2,
880
928
  name: "prune",
881
929
  },
930
+ {
931
+ brand: "boolean",
932
+ description: "Reject the 'any' type passed as an argument to the 'expect()' function or a matcher.",
933
+ group: 4,
934
+ name: "rejectAnyType",
935
+ },
936
+ {
937
+ brand: "boolean",
938
+ description: "Reject the 'never' type passed as an argument to the 'expect()' function or a matcher.",
939
+ group: 4,
940
+ name: "rejectNeverType",
941
+ },
882
942
  {
883
943
  brand: "list",
884
944
  description: "The list of reporters to use.",
@@ -914,7 +974,6 @@ class Options {
914
974
  items: {
915
975
  brand: "string",
916
976
  name: "target",
917
- pattern: "^([45]\\.[0-9](\\.[0-9])?)|beta|current|latest|next|rc$",
918
977
  },
919
978
  name: "target",
920
979
  },
@@ -1025,14 +1084,22 @@ class Options {
1025
1084
  }
1026
1085
  onDiagnostics(Diagnostic.error(ConfigDiagnosticText.moduleWasNotFound(optionValue), origin));
1027
1086
  break;
1028
- case "target":
1087
+ case "target": {
1088
+ if (/[<>=]/.test(optionValue)) {
1089
+ if (!Target.isRange(optionValue)) {
1090
+ onDiagnostics(Diagnostic.error([ConfigDiagnosticText.rangeIsNotValid(optionValue), ...ConfigDiagnosticText.rangeUsage()], origin));
1091
+ }
1092
+ break;
1093
+ }
1029
1094
  if ((await Store.validateTag(optionValue)) === false) {
1030
1095
  onDiagnostics(Diagnostic.error([
1031
1096
  ConfigDiagnosticText.versionIsNotSupported(optionValue),
1032
- await ConfigDiagnosticText.usage(optionName, optionBrand),
1033
- ].flat(), origin));
1097
+ ...ConfigDiagnosticText.usage(optionName, optionBrand),
1098
+ ConfigDiagnosticText.inspectSupportedVersions(),
1099
+ ], origin));
1034
1100
  }
1035
1101
  break;
1102
+ }
1036
1103
  case "testFileMatch":
1037
1104
  for (const segment of ["/", "../"]) {
1038
1105
  if (optionValue.startsWith(segment)) {
@@ -1311,13 +1378,13 @@ class ConfigFileParser {
1311
1378
  break;
1312
1379
  }
1313
1380
  case "list": {
1381
+ optionValue = [];
1314
1382
  const leftBracketToken = this.#jsonScanner.readToken("[");
1315
1383
  if (!leftBracketToken.text) {
1316
1384
  jsonNode = this.#jsonScanner.read();
1317
1385
  this.#onRequiresValue(optionDefinition, jsonNode, isListItem);
1318
1386
  break;
1319
1387
  }
1320
- optionValue = [];
1321
1388
  while (!this.#jsonScanner.isRead()) {
1322
1389
  if (this.#jsonScanner.peekToken("]")) {
1323
1390
  break;
@@ -1409,6 +1476,8 @@ class ConfigFileParser {
1409
1476
  const defaultOptions = {
1410
1477
  failFast: false,
1411
1478
  plugins: [],
1479
+ rejectAnyType: false,
1480
+ rejectNeverType: false,
1412
1481
  reporters: ["list", "summary"],
1413
1482
  rootPath: Path.resolve("./"),
1414
1483
  target: environmentOptions.typescriptModule != null ? ["current"] : ["latest"],
@@ -1425,6 +1494,9 @@ class Config {
1425
1494
  const pathMatch = [];
1426
1495
  const commandLineParser = new CommandLineParser(commandLineOptions, pathMatch, Config.#onDiagnostics);
1427
1496
  await commandLineParser.parse(commandLine);
1497
+ if (commandLineOptions.target != null) {
1498
+ commandLineOptions.target = await Target.expand(commandLineOptions.target);
1499
+ }
1428
1500
  return { commandLineOptions, pathMatch };
1429
1501
  }
1430
1502
  static async parseConfigFile(filePath) {
@@ -1439,6 +1511,9 @@ class Config {
1439
1511
  const sourceFile = new SourceFile(configFilePath, configFileText);
1440
1512
  const configFileParser = new ConfigFileParser(configFileOptions, sourceFile, Config.#onDiagnostics);
1441
1513
  await configFileParser.parse();
1514
+ if (configFileOptions.target != null) {
1515
+ configFileOptions.target = await Target.expand(configFileOptions.target);
1516
+ }
1442
1517
  }
1443
1518
  return { configFileOptions, configFilePath };
1444
1519
  }
@@ -2182,7 +2257,7 @@ function usesCompilerText(compilerVersion, projectConfigFilePath, options) {
2182
2257
  if (projectConfigFilePath != null) {
2183
2258
  projectConfigPathText = (jsx(Text, { color: "90", children: [" with ", Path.relative("", projectConfigFilePath)] }));
2184
2259
  }
2185
- return (jsx(Text, { children: [options?.prependEmptyLine === true ? jsx(Line, {}) : undefined, jsx(Line, { children: [jsx(Text, { color: "34", children: "uses" }), " TypeScript ", compilerVersion, projectConfigPathText] }), jsx(Line, {})] }));
2260
+ return (jsx(Text, { children: [options?.prependEmptyLine ? jsx(Line, {}) : undefined, jsx(Line, { children: [jsx(Text, { color: "34", children: "uses" }), " TypeScript ", compilerVersion, projectConfigPathText] }), jsx(Line, {})] }));
2186
2261
  }
2187
2262
 
2188
2263
  function waitingForFileChangesText() {
@@ -2255,7 +2330,7 @@ class FileView {
2255
2330
  return this.#messages;
2256
2331
  }
2257
2332
  getViewText(options) {
2258
- return fileViewText(this.#lines, options?.appendEmptyLine === true || this.hasErrors);
2333
+ return fileViewText(this.#lines, options?.appendEmptyLine || this.hasErrors);
2259
2334
  }
2260
2335
  }
2261
2336
 
@@ -2413,7 +2488,7 @@ class SetupReporter {
2413
2488
 
2414
2489
  class SummaryReporter extends BaseReporter {
2415
2490
  on([event, payload]) {
2416
- if (this.resolvedConfig.watch === true) {
2491
+ if (this.resolvedConfig.watch) {
2417
2492
  return;
2418
2493
  }
2419
2494
  if (event === "run:end") {
@@ -2625,6 +2700,28 @@ class SelectDiagnosticText {
2625
2700
 
2626
2701
  class Select {
2627
2702
  static #patternsCache = new WeakMap();
2703
+ static async #getAccessibleFileSystemEntries(targetPath) {
2704
+ const directories = [];
2705
+ const files = [];
2706
+ try {
2707
+ const entries = await fs.readdir(targetPath, { withFileTypes: true });
2708
+ for (const entry of entries) {
2709
+ let entryMeta = entry;
2710
+ if (entry.isSymbolicLink()) {
2711
+ entryMeta = await fs.stat([targetPath, entry.name].join("/"));
2712
+ }
2713
+ if (entryMeta.isDirectory()) {
2714
+ directories.push(entry.name);
2715
+ }
2716
+ else if (entryMeta.isFile()) {
2717
+ files.push(entry.name);
2718
+ }
2719
+ }
2720
+ }
2721
+ catch {
2722
+ }
2723
+ return { directories, files };
2724
+ }
2628
2725
  static #getMatchPatterns(globPatterns) {
2629
2726
  let matchPatterns = Select.#patternsCache.get(globPatterns);
2630
2727
  if (!matchPatterns) {
@@ -2653,18 +2750,6 @@ class Select {
2653
2750
  static #onDiagnostics(diagnostic) {
2654
2751
  EventEmitter.dispatch(["select:error", { diagnostics: [diagnostic] }]);
2655
2752
  }
2656
- static async #resolveEntryMeta(entry, targetPath) {
2657
- if (!entry.isSymbolicLink()) {
2658
- return entry;
2659
- }
2660
- let entryMeta;
2661
- try {
2662
- entryMeta = await fs.stat([targetPath, entry.name].join("/"));
2663
- }
2664
- catch {
2665
- }
2666
- return entryMeta;
2667
- }
2668
2753
  static async selectFiles(resolvedConfig) {
2669
2754
  const matchPatterns = Select.#getMatchPatterns(resolvedConfig.testFileMatch);
2670
2755
  const testFilePaths = [];
@@ -2676,20 +2761,18 @@ class Select {
2676
2761
  }
2677
2762
  static async #visitDirectory(currentPath, testFilePaths, matchPatterns, resolvedConfig) {
2678
2763
  const targetPath = Path.join(resolvedConfig.rootPath, currentPath);
2679
- try {
2680
- const entries = await fs.readdir(targetPath, { withFileTypes: true });
2681
- for (const entry of entries) {
2682
- const entryMeta = await Select.#resolveEntryMeta(entry, targetPath);
2683
- const entryPath = [currentPath, entry.name].join("/");
2684
- if (entryMeta?.isDirectory() && Select.#isDirectoryIncluded(entryPath, matchPatterns)) {
2685
- await Select.#visitDirectory(entryPath, testFilePaths, matchPatterns, resolvedConfig);
2686
- }
2687
- else if (entryMeta?.isFile() && Select.#isFileIncluded(entryPath, matchPatterns, resolvedConfig)) {
2688
- testFilePaths.push([targetPath, entry.name].join("/"));
2689
- }
2764
+ const entries = await Select.#getAccessibleFileSystemEntries(targetPath);
2765
+ for (const directoryName of entries.directories) {
2766
+ const directoryPath = [currentPath, directoryName].join("/");
2767
+ if (Select.#isDirectoryIncluded(directoryPath, matchPatterns)) {
2768
+ await Select.#visitDirectory(directoryPath, testFilePaths, matchPatterns, resolvedConfig);
2690
2769
  }
2691
2770
  }
2692
- catch {
2771
+ for (const fileName of entries.files) {
2772
+ const filePath = [currentPath, fileName].join("/");
2773
+ if (Select.#isFileIncluded(filePath, matchPatterns, resolvedConfig)) {
2774
+ testFilePaths.push([targetPath, fileName].join("/"));
2775
+ }
2693
2776
  }
2694
2777
  }
2695
2778
  }
@@ -2871,14 +2954,20 @@ class TestMember {
2871
2954
 
2872
2955
  class Assertion extends TestMember {
2873
2956
  isNot;
2957
+ matcherName;
2874
2958
  matcherNode;
2875
2959
  modifierNode;
2876
2960
  notNode;
2961
+ source;
2962
+ target;
2877
2963
  constructor(compiler, brand, node, parent, flags, matcherNode, modifierNode, notNode) {
2878
2964
  super(compiler, brand, node, parent, flags);
2879
2965
  this.isNot = notNode != null;
2966
+ this.matcherName = matcherNode.expression.name;
2880
2967
  this.matcherNode = matcherNode;
2881
2968
  this.modifierNode = modifierNode;
2969
+ this.source = this.node.typeArguments ?? this.node.arguments;
2970
+ this.target = this.matcherNode.typeArguments ?? this.matcherNode.arguments;
2882
2971
  for (const diagnostic of parent.diagnostics) {
2883
2972
  if (diagnostic.start != null && diagnostic.start >= this.source.pos && diagnostic.start <= this.source.end) {
2884
2973
  this.diagnostics.add(diagnostic);
@@ -2886,15 +2975,6 @@ class Assertion extends TestMember {
2886
2975
  }
2887
2976
  }
2888
2977
  }
2889
- get matcherName() {
2890
- return this.matcherNode.expression.name;
2891
- }
2892
- get source() {
2893
- return this.node.typeArguments ?? this.node.arguments;
2894
- }
2895
- get target() {
2896
- return this.matcherNode.typeArguments ?? this.matcherNode.arguments;
2897
- }
2898
2978
  }
2899
2979
 
2900
2980
  class IdentifierLookup {
@@ -3178,7 +3258,16 @@ class ProjectService {
3178
3258
  }
3179
3259
  }
3180
3260
 
3261
+ class Format {
3262
+ static capitalize(text) {
3263
+ return text.replace(/^./, text.charAt(0).toUpperCase());
3264
+ }
3265
+ }
3266
+
3181
3267
  class ExpectDiagnosticText {
3268
+ static argumentCannotBeOfType(argumentNameText, typeText) {
3269
+ return `An argument for '${argumentNameText}' cannot be of the '${typeText}' type.`;
3270
+ }
3182
3271
  static argumentOrTypeArgumentMustBeProvided(argumentNameText, typeArgumentNameText) {
3183
3272
  return `An argument for '${argumentNameText}' or type argument for '${typeArgumentNameText}' must be provided.`;
3184
3273
  }
@@ -3209,6 +3298,9 @@ class ExpectDiagnosticText {
3209
3298
  static raisedTypeError(count = 1) {
3210
3299
  return `The raised type error${count === 1 ? "" : "s"}:`;
3211
3300
  }
3301
+ static typeArgumentCannotBeOfType(argumentNameText, typeText) {
3302
+ return `A type argument for '${argumentNameText}' cannot be of the '${typeText}' type.`;
3303
+ }
3212
3304
  static typeArgumentMustBe(argumentNameText, expectedText) {
3213
3305
  return `A type argument for '${argumentNameText}' must be ${expectedText}.`;
3214
3306
  }
@@ -3270,6 +3362,13 @@ class ExpectDiagnosticText {
3270
3362
  static typesOfPropertyAreNotCompatible(propertyNameText) {
3271
3363
  return `Types of property '${propertyNameText}' are not compatible.`;
3272
3364
  }
3365
+ static typeWasRejected(typeText) {
3366
+ const optionNameText = `reject${Format.capitalize(typeText)}Type`;
3367
+ return [
3368
+ `The '${typeText}' type was rejected because the '${optionNameText}' option is enabled.`,
3369
+ `If this check is necessary, pass '${typeText}' as the type argument explicitly.`,
3370
+ ];
3371
+ }
3273
3372
  }
3274
3373
 
3275
3374
  class MatchWorker {
@@ -3371,9 +3470,6 @@ class MatchWorker {
3371
3470
  }
3372
3471
  return type;
3373
3472
  }
3374
- isAnyOrNeverType(type) {
3375
- return !!(type.flags & (this.#compiler.TypeFlags.Any | this.#compiler.TypeFlags.Never));
3376
- }
3377
3473
  isStringOrNumberLiteralType(type) {
3378
3474
  return !!(type.flags & this.#compiler.TypeFlags.StringOrNumberLiteral);
3379
3475
  }
@@ -3670,7 +3766,8 @@ class ToHaveProperty {
3670
3766
  match(matchWorker, sourceNode, targetNode, onDiagnostics) {
3671
3767
  const diagnostics = [];
3672
3768
  const sourceType = matchWorker.getType(sourceNode);
3673
- if (matchWorker.isAnyOrNeverType(sourceType) || !matchWorker.extendsObjectType(sourceType)) {
3769
+ if (sourceType.flags & (this.#compiler.TypeFlags.Any | this.#compiler.TypeFlags.Never) ||
3770
+ !matchWorker.extendsObjectType(sourceType)) {
3674
3771
  const expectedText = "of an object type";
3675
3772
  const text = this.#compiler.isTypeNode(sourceNode)
3676
3773
  ? ExpectDiagnosticText.typeArgumentMustBe("Source", expectedText)
@@ -3798,6 +3895,7 @@ class ToRaiseError {
3798
3895
 
3799
3896
  class ExpectService {
3800
3897
  #compiler;
3898
+ #rejectTypes = new Set();
3801
3899
  #typeChecker;
3802
3900
  toAcceptProps;
3803
3901
  toBe;
@@ -3818,9 +3916,15 @@ class ExpectService {
3818
3916
  toHaveProperty;
3819
3917
  toMatch;
3820
3918
  toRaiseError;
3821
- constructor(compiler, typeChecker) {
3919
+ constructor(compiler, typeChecker, resolvedConfig) {
3822
3920
  this.#compiler = compiler;
3823
3921
  this.#typeChecker = typeChecker;
3922
+ if (resolvedConfig?.rejectAnyType) {
3923
+ this.#rejectTypes.add("any");
3924
+ }
3925
+ if (resolvedConfig?.rejectNeverType) {
3926
+ this.#rejectTypes.add("never");
3927
+ }
3824
3928
  this.toAcceptProps = new ToAcceptProps(compiler, typeChecker);
3825
3929
  this.toBe = new ToBe();
3826
3930
  this.toBeAny = new PrimitiveTypeMatcher(compiler.TypeFlags.Any);
@@ -3863,6 +3967,9 @@ class ExpectService {
3863
3967
  this.#onTargetArgumentOrTypeArgumentMustBeProvided(assertion, onDiagnostics);
3864
3968
  return;
3865
3969
  }
3970
+ if (this.#rejectsTypeArguments(matchWorker, onDiagnostics)) {
3971
+ return;
3972
+ }
3866
3973
  return this[matcherNameText].match(matchWorker, assertion.source[0], assertion.target[0], onDiagnostics);
3867
3974
  case "toBeAny":
3868
3975
  case "toBeBigInt":
@@ -3884,6 +3991,9 @@ class ExpectService {
3884
3991
  }
3885
3992
  return this.toHaveProperty.match(matchWorker, assertion.source[0], assertion.target[0], onDiagnostics);
3886
3993
  case "toRaiseError":
3994
+ if (assertion.isNot && this.#rejectsTypeArguments(matchWorker, onDiagnostics)) {
3995
+ return;
3996
+ }
3887
3997
  return this.toRaiseError.match(matchWorker, assertion.source[0], [...assertion.target], onDiagnostics);
3888
3998
  default:
3889
3999
  this.#onMatcherIsNotSupported(matcherNameText, assertion, onDiagnostics);
@@ -3910,6 +4020,29 @@ class ExpectService {
3910
4020
  const origin = DiagnosticOrigin.fromNode(assertion.matcherName);
3911
4021
  onDiagnostics(Diagnostic.error(text, origin));
3912
4022
  }
4023
+ #rejectsTypeArguments(matchWorker, onDiagnostics) {
4024
+ for (const rejectedType of this.#rejectTypes) {
4025
+ for (const argumentName of ["source", "target"]) {
4026
+ const argumentNode = matchWorker.assertion[argumentName][0];
4027
+ if (!argumentNode ||
4028
+ argumentNode.kind === this.#compiler.SyntaxKind[`${Format.capitalize(rejectedType)}Keyword`]) {
4029
+ continue;
4030
+ }
4031
+ if (matchWorker.getType(argumentNode).flags & this.#compiler.TypeFlags[Format.capitalize(rejectedType)]) {
4032
+ const text = [
4033
+ this.#compiler.isTypeNode(argumentNode)
4034
+ ? ExpectDiagnosticText.typeArgumentCannotBeOfType(Format.capitalize(argumentName), rejectedType)
4035
+ : ExpectDiagnosticText.argumentCannotBeOfType(argumentName, rejectedType),
4036
+ ...ExpectDiagnosticText.typeWasRejected(rejectedType),
4037
+ ];
4038
+ const origin = DiagnosticOrigin.fromNode(argumentNode);
4039
+ onDiagnostics(Diagnostic.error(text, origin));
4040
+ return true;
4041
+ }
4042
+ }
4043
+ }
4044
+ return false;
4045
+ }
3913
4046
  }
3914
4047
 
3915
4048
  class TestTreeWalker {
@@ -3927,7 +4060,7 @@ class TestTreeWalker {
3927
4060
  this.#hasOnly = options.hasOnly || resolvedConfig.only != null || options.position != null;
3928
4061
  this.#position = options.position;
3929
4062
  this.#taskResult = options.taskResult;
3930
- this.#expectService = new ExpectService(compiler, typeChecker);
4063
+ this.#expectService = new ExpectService(compiler, typeChecker, this.#resolvedConfig);
3931
4064
  }
3932
4065
  #resolveRunMode(mode, member) {
3933
4066
  if (member.flags & 1) {
@@ -3952,7 +4085,7 @@ class TestTreeWalker {
3952
4085
  }
3953
4086
  visit(members, runMode, parentResult) {
3954
4087
  for (const member of members) {
3955
- if (this.#cancellationToken?.isCancellationRequested === true) {
4088
+ if (this.#cancellationToken?.isCancellationRequested) {
3956
4089
  break;
3957
4090
  }
3958
4091
  const validationError = member.validate();
@@ -4076,7 +4209,7 @@ class TaskRunner {
4076
4209
  this.#projectService = new ProjectService(this.#resolvedConfig, compiler);
4077
4210
  }
4078
4211
  run(task, cancellationToken) {
4079
- if (cancellationToken?.isCancellationRequested === true) {
4212
+ if (cancellationToken?.isCancellationRequested) {
4080
4213
  return;
4081
4214
  }
4082
4215
  this.#projectService.openFile(task.filePath, undefined, this.#resolvedConfig.rootPath);
@@ -4137,7 +4270,7 @@ class TaskRunner {
4137
4270
  class Runner {
4138
4271
  #eventEmitter = new EventEmitter();
4139
4272
  #resolvedConfig;
4140
- static version = "3.0.0";
4273
+ static version = "3.1.1";
4141
4274
  constructor(resolvedConfig) {
4142
4275
  this.#resolvedConfig = resolvedConfig;
4143
4276
  }
@@ -4161,7 +4294,7 @@ class Runner {
4161
4294
  }
4162
4295
  }
4163
4296
  }
4164
- if (this.#resolvedConfig.watch === true) {
4297
+ if (this.#resolvedConfig.watch) {
4165
4298
  const watchReporter = new WatchReporter(this.#resolvedConfig);
4166
4299
  this.#eventEmitter.addReporter(watchReporter);
4167
4300
  }
@@ -4176,7 +4309,7 @@ class Runner {
4176
4309
  this.#eventEmitter.addHandler(cancellationHandler);
4177
4310
  }
4178
4311
  await this.#run(tasks, cancellationToken);
4179
- if (this.#resolvedConfig.watch === true) {
4312
+ if (this.#resolvedConfig.watch) {
4180
4313
  await this.#watch(tasks, cancellationToken);
4181
4314
  }
4182
4315
  this.#eventEmitter.removeReporters();
@@ -4228,6 +4361,13 @@ class Cli {
4228
4361
  OutputService.writeMessage(formattedText(Runner.version));
4229
4362
  return;
4230
4363
  }
4364
+ if (commandLine.includes("--list")) {
4365
+ await Store.open();
4366
+ if (Store.manifest != null) {
4367
+ OutputService.writeMessage(formattedText({ resolutions: Store.manifest.resolutions, versions: Store.manifest.versions }));
4368
+ }
4369
+ return;
4370
+ }
4231
4371
  if (commandLine.includes("--prune")) {
4232
4372
  await Store.prune();
4233
4373
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tstyche",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "The Essential Type Testing Tool.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -63,17 +63,17 @@
63
63
  "devDependencies": {
64
64
  "@biomejs/biome": "1.9.4",
65
65
  "@rollup/plugin-typescript": "12.1.1",
66
- "@types/node": "22.9.0",
67
- "@types/react": "18.3.12",
66
+ "@types/node": "22.10.1",
67
+ "@types/react": "18.3.13",
68
68
  "ajv": "8.17.1",
69
- "cspell": "8.15.7",
70
- "magic-string": "0.30.12",
71
- "monocart-coverage-reports": "2.11.1",
72
- "pretty-ansi": "2.0.0",
73
- "rollup": "4.24.4",
69
+ "cspell": "8.16.1",
70
+ "magic-string": "0.30.14",
71
+ "monocart-coverage-reports": "2.11.3",
72
+ "pretty-ansi": "3.0.0",
73
+ "rollup": "4.28.0",
74
74
  "rollup-plugin-dts": "6.1.1",
75
75
  "tslib": "2.8.1",
76
- "typescript": "5.6.3"
76
+ "typescript": "5.7.2"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "typescript": "4.x || 5.x"
@@ -83,7 +83,7 @@
83
83
  "optional": true
84
84
  }
85
85
  },
86
- "packageManager": "yarn@4.5.1",
86
+ "packageManager": "yarn@4.5.3",
87
87
  "engines": {
88
88
  "node": ">=18.19"
89
89
  }