tstyche 3.0.0-rc.2 → 3.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 CHANGED
@@ -57,7 +57,7 @@ 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.
@@ -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,39 @@ 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 expand(queries) {
828
+ const include = [];
829
+ for (const query of queries) {
830
+ if (!Target.isRange(query)) {
831
+ include.push(query);
832
+ continue;
833
+ }
834
+ if (Store.manifest != null) {
835
+ let versions = Object.keys(Store.manifest.resolutions).slice(0, -4);
836
+ for (const comparator of query.split(" ")) {
837
+ versions = Target.#filter(comparator, versions);
805
838
  }
806
- return text;
839
+ include.push(...versions);
807
840
  }
808
841
  }
809
- return [ConfigDiagnosticText.requiresValueType(optionName, optionBrand)];
842
+ return include;
810
843
  }
811
- static versionIsNotSupported(value) {
812
- if (value === "current") {
813
- return "Cannot use 'current' as a target. Failed to resolve the installed TypeScript module.";
844
+ static #filter(comparator, versions) {
845
+ const targetVersionIndex = versions.findIndex((version) => version === comparator.replace(/^[<>]=?/, ""));
846
+ if (targetVersionIndex !== -1) {
847
+ switch (comparator.charAt(0)) {
848
+ case ">":
849
+ return versions.slice(comparator.charAt(1) === "=" ? targetVersionIndex : targetVersionIndex + 1);
850
+ case "<":
851
+ return versions.slice(0, comparator.charAt(1) === "=" ? targetVersionIndex + 1 : targetVersionIndex);
852
+ }
814
853
  }
815
- return `TypeScript version '${value}' is not supported.`;
854
+ return [];
816
855
  }
817
- static watchCannotBeEnabled() {
818
- return "Watch mode cannot be enabled in continuous integration environment.";
856
+ static isRange(query) {
857
+ return Target.#rangeRegex.test(query);
819
858
  }
820
859
  }
821
860
 
@@ -851,6 +890,12 @@ class Options {
851
890
  group: 2,
852
891
  name: "install",
853
892
  },
893
+ {
894
+ brand: "bareTrue",
895
+ description: "Print the list of supported versions of the 'typescript' package and exit.",
896
+ group: 2,
897
+ name: "list",
898
+ },
854
899
  {
855
900
  brand: "bareTrue",
856
901
  description: "Print the list of the selected test files and exit.",
@@ -879,6 +924,18 @@ class Options {
879
924
  group: 2,
880
925
  name: "prune",
881
926
  },
927
+ {
928
+ brand: "boolean",
929
+ description: "Reject the 'any' type passed as an argument to the 'expect()' function or a matcher.",
930
+ group: 4,
931
+ name: "rejectAnyType",
932
+ },
933
+ {
934
+ brand: "boolean",
935
+ description: "Reject the 'never' type passed as an argument to the 'expect()' function or a matcher.",
936
+ group: 4,
937
+ name: "rejectNeverType",
938
+ },
882
939
  {
883
940
  brand: "list",
884
941
  description: "The list of reporters to use.",
@@ -914,7 +971,6 @@ class Options {
914
971
  items: {
915
972
  brand: "string",
916
973
  name: "target",
917
- pattern: "^([45]\\.[0-9](\\.[0-9])?)|beta|current|latest|next|rc$",
918
974
  },
919
975
  name: "target",
920
976
  },
@@ -1025,14 +1081,33 @@ class Options {
1025
1081
  }
1026
1082
  onDiagnostics(Diagnostic.error(ConfigDiagnosticText.moduleWasNotFound(optionValue), origin));
1027
1083
  break;
1028
- case "target":
1084
+ case "target": {
1085
+ if (/[<>=]/.test(optionValue)) {
1086
+ if (Target.isRange(optionValue)) {
1087
+ for (const value of optionValue.split(" ").map((value) => value.replace(/^[<>]=?/, ""))) {
1088
+ if ((await Store.validateTag(value)) === false) {
1089
+ onDiagnostics(Diagnostic.error([
1090
+ ConfigDiagnosticText.versionIsNotSupported(value),
1091
+ ...ConfigDiagnosticText.usage(optionName, optionBrand),
1092
+ ConfigDiagnosticText.inspectSupportedVersions(),
1093
+ ], origin));
1094
+ }
1095
+ }
1096
+ }
1097
+ else {
1098
+ onDiagnostics(Diagnostic.error([ConfigDiagnosticText.rangeIsNotValid(optionValue), ...ConfigDiagnosticText.rangeUsage()], origin));
1099
+ }
1100
+ break;
1101
+ }
1029
1102
  if ((await Store.validateTag(optionValue)) === false) {
1030
1103
  onDiagnostics(Diagnostic.error([
1031
1104
  ConfigDiagnosticText.versionIsNotSupported(optionValue),
1032
- await ConfigDiagnosticText.usage(optionName, optionBrand),
1033
- ].flat(), origin));
1105
+ ...ConfigDiagnosticText.usage(optionName, optionBrand),
1106
+ ConfigDiagnosticText.inspectSupportedVersions(),
1107
+ ], origin));
1034
1108
  }
1035
1109
  break;
1110
+ }
1036
1111
  case "testFileMatch":
1037
1112
  for (const segment of ["/", "../"]) {
1038
1113
  if (optionValue.startsWith(segment)) {
@@ -1311,13 +1386,13 @@ class ConfigFileParser {
1311
1386
  break;
1312
1387
  }
1313
1388
  case "list": {
1389
+ optionValue = [];
1314
1390
  const leftBracketToken = this.#jsonScanner.readToken("[");
1315
1391
  if (!leftBracketToken.text) {
1316
1392
  jsonNode = this.#jsonScanner.read();
1317
1393
  this.#onRequiresValue(optionDefinition, jsonNode, isListItem);
1318
1394
  break;
1319
1395
  }
1320
- optionValue = [];
1321
1396
  while (!this.#jsonScanner.isRead()) {
1322
1397
  if (this.#jsonScanner.peekToken("]")) {
1323
1398
  break;
@@ -1409,6 +1484,8 @@ class ConfigFileParser {
1409
1484
  const defaultOptions = {
1410
1485
  failFast: false,
1411
1486
  plugins: [],
1487
+ rejectAnyType: false,
1488
+ rejectNeverType: false,
1412
1489
  reporters: ["list", "summary"],
1413
1490
  rootPath: Path.resolve("./"),
1414
1491
  target: environmentOptions.typescriptModule != null ? ["current"] : ["latest"],
@@ -1453,6 +1530,7 @@ class Config {
1453
1530
  if ("config" in resolvedConfig) {
1454
1531
  delete resolvedConfig.config;
1455
1532
  }
1533
+ resolvedConfig.target = Target.expand(resolvedConfig.target);
1456
1534
  return resolvedConfig;
1457
1535
  }
1458
1536
  static resolveConfigFilePath(filePath) {
@@ -2182,7 +2260,7 @@ function usesCompilerText(compilerVersion, projectConfigFilePath, options) {
2182
2260
  if (projectConfigFilePath != null) {
2183
2261
  projectConfigPathText = (jsx(Text, { color: "90", children: [" with ", Path.relative("", projectConfigFilePath)] }));
2184
2262
  }
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, {})] }));
2263
+ return (jsx(Text, { children: [options?.prependEmptyLine ? jsx(Line, {}) : undefined, jsx(Line, { children: [jsx(Text, { color: "34", children: "uses" }), " TypeScript ", compilerVersion, projectConfigPathText] }), jsx(Line, {})] }));
2186
2264
  }
2187
2265
 
2188
2266
  function waitingForFileChangesText() {
@@ -2255,7 +2333,7 @@ class FileView {
2255
2333
  return this.#messages;
2256
2334
  }
2257
2335
  getViewText(options) {
2258
- return fileViewText(this.#lines, options?.appendEmptyLine === true || this.hasErrors);
2336
+ return fileViewText(this.#lines, options?.appendEmptyLine || this.hasErrors);
2259
2337
  }
2260
2338
  }
2261
2339
 
@@ -2413,7 +2491,7 @@ class SetupReporter {
2413
2491
 
2414
2492
  class SummaryReporter extends BaseReporter {
2415
2493
  on([event, payload]) {
2416
- if (this.resolvedConfig.watch === true) {
2494
+ if (this.resolvedConfig.watch) {
2417
2495
  return;
2418
2496
  }
2419
2497
  if (event === "run:end") {
@@ -2625,6 +2703,28 @@ class SelectDiagnosticText {
2625
2703
 
2626
2704
  class Select {
2627
2705
  static #patternsCache = new WeakMap();
2706
+ static async #getAccessibleFileSystemEntries(targetPath) {
2707
+ const directories = [];
2708
+ const files = [];
2709
+ try {
2710
+ const entries = await fs.readdir(targetPath, { withFileTypes: true });
2711
+ for (const entry of entries) {
2712
+ let entryMeta = entry;
2713
+ if (entry.isSymbolicLink()) {
2714
+ entryMeta = await fs.stat([targetPath, entry.name].join("/"));
2715
+ }
2716
+ if (entryMeta.isDirectory()) {
2717
+ directories.push(entry.name);
2718
+ }
2719
+ else if (entryMeta.isFile()) {
2720
+ files.push(entry.name);
2721
+ }
2722
+ }
2723
+ }
2724
+ catch {
2725
+ }
2726
+ return { directories, files };
2727
+ }
2628
2728
  static #getMatchPatterns(globPatterns) {
2629
2729
  let matchPatterns = Select.#patternsCache.get(globPatterns);
2630
2730
  if (!matchPatterns) {
@@ -2653,18 +2753,6 @@ class Select {
2653
2753
  static #onDiagnostics(diagnostic) {
2654
2754
  EventEmitter.dispatch(["select:error", { diagnostics: [diagnostic] }]);
2655
2755
  }
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
2756
  static async selectFiles(resolvedConfig) {
2669
2757
  const matchPatterns = Select.#getMatchPatterns(resolvedConfig.testFileMatch);
2670
2758
  const testFilePaths = [];
@@ -2676,20 +2764,18 @@ class Select {
2676
2764
  }
2677
2765
  static async #visitDirectory(currentPath, testFilePaths, matchPatterns, resolvedConfig) {
2678
2766
  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
- }
2767
+ const entries = await Select.#getAccessibleFileSystemEntries(targetPath);
2768
+ for (const directoryName of entries.directories) {
2769
+ const directoryPath = [currentPath, directoryName].join("/");
2770
+ if (Select.#isDirectoryIncluded(directoryPath, matchPatterns)) {
2771
+ await Select.#visitDirectory(directoryPath, testFilePaths, matchPatterns, resolvedConfig);
2690
2772
  }
2691
2773
  }
2692
- catch {
2774
+ for (const fileName of entries.files) {
2775
+ const filePath = [currentPath, fileName].join("/");
2776
+ if (Select.#isFileIncluded(filePath, matchPatterns, resolvedConfig)) {
2777
+ testFilePaths.push([targetPath, fileName].join("/"));
2778
+ }
2693
2779
  }
2694
2780
  }
2695
2781
  }
@@ -2871,14 +2957,20 @@ class TestMember {
2871
2957
 
2872
2958
  class Assertion extends TestMember {
2873
2959
  isNot;
2960
+ matcherName;
2874
2961
  matcherNode;
2875
2962
  modifierNode;
2876
2963
  notNode;
2964
+ source;
2965
+ target;
2877
2966
  constructor(compiler, brand, node, parent, flags, matcherNode, modifierNode, notNode) {
2878
2967
  super(compiler, brand, node, parent, flags);
2879
2968
  this.isNot = notNode != null;
2969
+ this.matcherName = matcherNode.expression.name;
2880
2970
  this.matcherNode = matcherNode;
2881
2971
  this.modifierNode = modifierNode;
2972
+ this.source = this.node.typeArguments ?? this.node.arguments;
2973
+ this.target = this.matcherNode.typeArguments ?? this.matcherNode.arguments;
2882
2974
  for (const diagnostic of parent.diagnostics) {
2883
2975
  if (diagnostic.start != null && diagnostic.start >= this.source.pos && diagnostic.start <= this.source.end) {
2884
2976
  this.diagnostics.add(diagnostic);
@@ -2886,15 +2978,6 @@ class Assertion extends TestMember {
2886
2978
  }
2887
2979
  }
2888
2980
  }
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
2981
  }
2899
2982
 
2900
2983
  class IdentifierLookup {
@@ -3178,7 +3261,16 @@ class ProjectService {
3178
3261
  }
3179
3262
  }
3180
3263
 
3264
+ class Format {
3265
+ static capitalize(text) {
3266
+ return text.replace(/^./, text.charAt(0).toUpperCase());
3267
+ }
3268
+ }
3269
+
3181
3270
  class ExpectDiagnosticText {
3271
+ static argumentCannotBeOfType(argumentNameText, typeText) {
3272
+ return `An argument for '${argumentNameText}' cannot be of the '${typeText}' type.`;
3273
+ }
3182
3274
  static argumentOrTypeArgumentMustBeProvided(argumentNameText, typeArgumentNameText) {
3183
3275
  return `An argument for '${argumentNameText}' or type argument for '${typeArgumentNameText}' must be provided.`;
3184
3276
  }
@@ -3209,6 +3301,9 @@ class ExpectDiagnosticText {
3209
3301
  static raisedTypeError(count = 1) {
3210
3302
  return `The raised type error${count === 1 ? "" : "s"}:`;
3211
3303
  }
3304
+ static typeArgumentCannotBeOfType(argumentNameText, typeText) {
3305
+ return `A type argument for '${argumentNameText}' cannot be of the '${typeText}' type.`;
3306
+ }
3212
3307
  static typeArgumentMustBe(argumentNameText, expectedText) {
3213
3308
  return `A type argument for '${argumentNameText}' must be ${expectedText}.`;
3214
3309
  }
@@ -3270,6 +3365,13 @@ class ExpectDiagnosticText {
3270
3365
  static typesOfPropertyAreNotCompatible(propertyNameText) {
3271
3366
  return `Types of property '${propertyNameText}' are not compatible.`;
3272
3367
  }
3368
+ static typeWasRejected(typeText) {
3369
+ const optionNameText = `reject${Format.capitalize(typeText)}Type`;
3370
+ return [
3371
+ `The '${typeText}' type was rejected because the '${optionNameText}' option is enabled.`,
3372
+ `If this check is necessary, pass '${typeText}' as the type argument explicitly.`,
3373
+ ];
3374
+ }
3273
3375
  }
3274
3376
 
3275
3377
  class MatchWorker {
@@ -3371,9 +3473,6 @@ class MatchWorker {
3371
3473
  }
3372
3474
  return type;
3373
3475
  }
3374
- isAnyOrNeverType(type) {
3375
- return !!(type.flags & (this.#compiler.TypeFlags.Any | this.#compiler.TypeFlags.Never));
3376
- }
3377
3476
  isStringOrNumberLiteralType(type) {
3378
3477
  return !!(type.flags & this.#compiler.TypeFlags.StringOrNumberLiteral);
3379
3478
  }
@@ -3670,7 +3769,8 @@ class ToHaveProperty {
3670
3769
  match(matchWorker, sourceNode, targetNode, onDiagnostics) {
3671
3770
  const diagnostics = [];
3672
3771
  const sourceType = matchWorker.getType(sourceNode);
3673
- if (matchWorker.isAnyOrNeverType(sourceType) || !matchWorker.extendsObjectType(sourceType)) {
3772
+ if (sourceType.flags & (this.#compiler.TypeFlags.Any | this.#compiler.TypeFlags.Never) ||
3773
+ !matchWorker.extendsObjectType(sourceType)) {
3674
3774
  const expectedText = "of an object type";
3675
3775
  const text = this.#compiler.isTypeNode(sourceNode)
3676
3776
  ? ExpectDiagnosticText.typeArgumentMustBe("Source", expectedText)
@@ -3798,6 +3898,7 @@ class ToRaiseError {
3798
3898
 
3799
3899
  class ExpectService {
3800
3900
  #compiler;
3901
+ #rejectTypes = new Set();
3801
3902
  #typeChecker;
3802
3903
  toAcceptProps;
3803
3904
  toBe;
@@ -3818,9 +3919,15 @@ class ExpectService {
3818
3919
  toHaveProperty;
3819
3920
  toMatch;
3820
3921
  toRaiseError;
3821
- constructor(compiler, typeChecker) {
3922
+ constructor(compiler, typeChecker, resolvedConfig) {
3822
3923
  this.#compiler = compiler;
3823
3924
  this.#typeChecker = typeChecker;
3925
+ if (resolvedConfig?.rejectAnyType) {
3926
+ this.#rejectTypes.add("any");
3927
+ }
3928
+ if (resolvedConfig?.rejectNeverType) {
3929
+ this.#rejectTypes.add("never");
3930
+ }
3824
3931
  this.toAcceptProps = new ToAcceptProps(compiler, typeChecker);
3825
3932
  this.toBe = new ToBe();
3826
3933
  this.toBeAny = new PrimitiveTypeMatcher(compiler.TypeFlags.Any);
@@ -3863,6 +3970,9 @@ class ExpectService {
3863
3970
  this.#onTargetArgumentOrTypeArgumentMustBeProvided(assertion, onDiagnostics);
3864
3971
  return;
3865
3972
  }
3973
+ if (this.#rejectsTypeArguments(matchWorker, onDiagnostics)) {
3974
+ return;
3975
+ }
3866
3976
  return this[matcherNameText].match(matchWorker, assertion.source[0], assertion.target[0], onDiagnostics);
3867
3977
  case "toBeAny":
3868
3978
  case "toBeBigInt":
@@ -3884,6 +3994,9 @@ class ExpectService {
3884
3994
  }
3885
3995
  return this.toHaveProperty.match(matchWorker, assertion.source[0], assertion.target[0], onDiagnostics);
3886
3996
  case "toRaiseError":
3997
+ if (assertion.isNot && this.#rejectsTypeArguments(matchWorker, onDiagnostics)) {
3998
+ return;
3999
+ }
3887
4000
  return this.toRaiseError.match(matchWorker, assertion.source[0], [...assertion.target], onDiagnostics);
3888
4001
  default:
3889
4002
  this.#onMatcherIsNotSupported(matcherNameText, assertion, onDiagnostics);
@@ -3910,6 +4023,29 @@ class ExpectService {
3910
4023
  const origin = DiagnosticOrigin.fromNode(assertion.matcherName);
3911
4024
  onDiagnostics(Diagnostic.error(text, origin));
3912
4025
  }
4026
+ #rejectsTypeArguments(matchWorker, onDiagnostics) {
4027
+ for (const rejectedType of this.#rejectTypes) {
4028
+ for (const argumentName of ["source", "target"]) {
4029
+ const argumentNode = matchWorker.assertion[argumentName][0];
4030
+ if (!argumentNode ||
4031
+ argumentNode.kind === this.#compiler.SyntaxKind[`${Format.capitalize(rejectedType)}Keyword`]) {
4032
+ continue;
4033
+ }
4034
+ if (matchWorker.getType(argumentNode).flags & this.#compiler.TypeFlags[Format.capitalize(rejectedType)]) {
4035
+ const text = [
4036
+ this.#compiler.isTypeNode(argumentNode)
4037
+ ? ExpectDiagnosticText.typeArgumentCannotBeOfType(Format.capitalize(argumentName), rejectedType)
4038
+ : ExpectDiagnosticText.argumentCannotBeOfType(argumentName, rejectedType),
4039
+ ...ExpectDiagnosticText.typeWasRejected(rejectedType),
4040
+ ];
4041
+ const origin = DiagnosticOrigin.fromNode(argumentNode);
4042
+ onDiagnostics(Diagnostic.error(text, origin));
4043
+ return true;
4044
+ }
4045
+ }
4046
+ }
4047
+ return false;
4048
+ }
3913
4049
  }
3914
4050
 
3915
4051
  class TestTreeWalker {
@@ -3927,7 +4063,7 @@ class TestTreeWalker {
3927
4063
  this.#hasOnly = options.hasOnly || resolvedConfig.only != null || options.position != null;
3928
4064
  this.#position = options.position;
3929
4065
  this.#taskResult = options.taskResult;
3930
- this.#expectService = new ExpectService(compiler, typeChecker);
4066
+ this.#expectService = new ExpectService(compiler, typeChecker, this.#resolvedConfig);
3931
4067
  }
3932
4068
  #resolveRunMode(mode, member) {
3933
4069
  if (member.flags & 1) {
@@ -3952,7 +4088,7 @@ class TestTreeWalker {
3952
4088
  }
3953
4089
  visit(members, runMode, parentResult) {
3954
4090
  for (const member of members) {
3955
- if (this.#cancellationToken?.isCancellationRequested === true) {
4091
+ if (this.#cancellationToken?.isCancellationRequested) {
3956
4092
  break;
3957
4093
  }
3958
4094
  const validationError = member.validate();
@@ -4076,7 +4212,7 @@ class TaskRunner {
4076
4212
  this.#projectService = new ProjectService(this.#resolvedConfig, compiler);
4077
4213
  }
4078
4214
  run(task, cancellationToken) {
4079
- if (cancellationToken?.isCancellationRequested === true) {
4215
+ if (cancellationToken?.isCancellationRequested) {
4080
4216
  return;
4081
4217
  }
4082
4218
  this.#projectService.openFile(task.filePath, undefined, this.#resolvedConfig.rootPath);
@@ -4137,7 +4273,7 @@ class TaskRunner {
4137
4273
  class Runner {
4138
4274
  #eventEmitter = new EventEmitter();
4139
4275
  #resolvedConfig;
4140
- static version = "3.0.0-rc.2";
4276
+ static version = "3.1.0";
4141
4277
  constructor(resolvedConfig) {
4142
4278
  this.#resolvedConfig = resolvedConfig;
4143
4279
  }
@@ -4161,7 +4297,7 @@ class Runner {
4161
4297
  }
4162
4298
  }
4163
4299
  }
4164
- if (this.#resolvedConfig.watch === true) {
4300
+ if (this.#resolvedConfig.watch) {
4165
4301
  const watchReporter = new WatchReporter(this.#resolvedConfig);
4166
4302
  this.#eventEmitter.addReporter(watchReporter);
4167
4303
  }
@@ -4176,7 +4312,7 @@ class Runner {
4176
4312
  this.#eventEmitter.addHandler(cancellationHandler);
4177
4313
  }
4178
4314
  await this.#run(tasks, cancellationToken);
4179
- if (this.#resolvedConfig.watch === true) {
4315
+ if (this.#resolvedConfig.watch) {
4180
4316
  await this.#watch(tasks, cancellationToken);
4181
4317
  }
4182
4318
  this.#eventEmitter.removeReporters();
@@ -4228,6 +4364,13 @@ class Cli {
4228
4364
  OutputService.writeMessage(formattedText(Runner.version));
4229
4365
  return;
4230
4366
  }
4367
+ if (commandLine.includes("--list")) {
4368
+ await Store.open();
4369
+ if (Store.manifest != null) {
4370
+ OutputService.writeMessage(formattedText({ resolutions: Store.manifest.resolutions, versions: Store.manifest.versions }));
4371
+ }
4372
+ return;
4373
+ }
4231
4374
  if (commandLine.includes("--prune")) {
4232
4375
  await Store.prune();
4233
4376
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tstyche",
3
- "version": "3.0.0-rc.2",
3
+ "version": "3.1.0",
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.8.6",
66
+ "@types/node": "22.10.1",
67
67
  "@types/react": "18.3.12",
68
68
  "ajv": "8.17.1",
69
- "cspell": "8.15.5",
70
- "magic-string": "0.30.12",
71
- "monocart-coverage-reports": "2.11.1",
72
- "pretty-ansi": "2.0.0",
73
- "rollup": "4.24.3",
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
  }