@schmock/core 1.0.3 → 1.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.
Files changed (39) hide show
  1. package/dist/builder.d.ts +13 -5
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +147 -60
  4. package/dist/constants.d.ts +6 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +20 -0
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +3 -1
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +20 -11
  12. package/dist/parser.d.ts.map +1 -1
  13. package/dist/parser.js +2 -17
  14. package/dist/types.d.ts +17 -210
  15. package/dist/types.d.ts.map +1 -1
  16. package/dist/types.js +1 -0
  17. package/package.json +4 -4
  18. package/src/builder.test.ts +2 -2
  19. package/src/builder.ts +232 -108
  20. package/src/constants.test.ts +59 -0
  21. package/src/constants.ts +25 -0
  22. package/src/errors.ts +3 -1
  23. package/src/index.ts +41 -29
  24. package/src/namespace.test.ts +3 -2
  25. package/src/parser.property.test.ts +495 -0
  26. package/src/parser.ts +2 -20
  27. package/src/route-matching.test.ts +1 -1
  28. package/src/steps/async-support.steps.ts +101 -91
  29. package/src/steps/basic-usage.steps.ts +49 -36
  30. package/src/steps/developer-experience.steps.ts +110 -94
  31. package/src/steps/error-handling.steps.ts +90 -66
  32. package/src/steps/fluent-api.steps.ts +75 -72
  33. package/src/steps/http-methods.steps.ts +33 -33
  34. package/src/steps/performance-reliability.steps.ts +52 -88
  35. package/src/steps/plugin-integration.steps.ts +176 -176
  36. package/src/steps/request-history.steps.ts +333 -0
  37. package/src/steps/state-concurrency.steps.ts +418 -316
  38. package/src/steps/stateful-workflows.steps.ts +138 -136
  39. package/src/types.ts +20 -259
package/src/builder.ts CHANGED
@@ -5,19 +5,10 @@ import {
5
5
  SchmockError,
6
6
  } from "./errors.js";
7
7
  import { parseRouteKey } from "./parser.js";
8
- import type {
9
- Generator,
10
- GeneratorFunction,
11
- GlobalConfig,
12
- HttpMethod,
13
- Plugin,
14
- PluginContext,
15
- RequestContext,
16
- RequestOptions,
17
- Response,
18
- RouteConfig,
19
- RouteKey,
20
- } from "./types.js";
8
+
9
+ function errorMessage(error: unknown): string {
10
+ return error instanceof Error ? error.message : "Unknown error";
11
+ }
21
12
 
22
13
  /**
23
14
  * Debug logger that respects debug mode configuration
@@ -25,7 +16,7 @@ import type {
25
16
  class DebugLogger {
26
17
  constructor(private enabled = false) {}
27
18
 
28
- log(category: string, message: string, data?: any) {
19
+ log(category: string, message: string, data?: unknown) {
29
20
  if (!this.enabled) return;
30
21
 
31
22
  const timestamp = new Date().toISOString();
@@ -55,10 +46,29 @@ class DebugLogger {
55
46
  interface CompiledCallableRoute {
56
47
  pattern: RegExp;
57
48
  params: string[];
58
- method: HttpMethod;
49
+ method: Schmock.HttpMethod;
59
50
  path: string;
60
- generator: Generator;
61
- config: RouteConfig;
51
+ generator: Schmock.Generator;
52
+ config: Schmock.RouteConfig;
53
+ }
54
+
55
+ function isGeneratorFunction(
56
+ gen: Schmock.Generator,
57
+ ): gen is Schmock.GeneratorFunction {
58
+ return typeof gen === "function";
59
+ }
60
+
61
+ function isResponseObject(value: unknown): value is {
62
+ status: number;
63
+ body: unknown;
64
+ headers?: Record<string, string>;
65
+ } {
66
+ return (
67
+ typeof value === "object" &&
68
+ value !== null &&
69
+ "status" in value &&
70
+ "body" in value
71
+ );
62
72
  }
63
73
 
64
74
  /**
@@ -68,10 +78,12 @@ interface CompiledCallableRoute {
68
78
  */
69
79
  export class CallableMockInstance {
70
80
  private routes: CompiledCallableRoute[] = [];
71
- private plugins: Plugin[] = [];
81
+ private staticRoutes = new Map<string, CompiledCallableRoute>();
82
+ private plugins: Schmock.Plugin[] = [];
72
83
  private logger: DebugLogger;
84
+ private requestHistory: Schmock.RequestRecord[] = [];
73
85
 
74
- constructor(private globalConfig: GlobalConfig = {}) {
86
+ constructor(private globalConfig: Schmock.GlobalConfig = {}) {
75
87
  this.logger = new DebugLogger(globalConfig.debug || false);
76
88
  if (globalConfig.debug) {
77
89
  this.logger.log("config", "Debug mode enabled");
@@ -85,9 +97,9 @@ export class CallableMockInstance {
85
97
 
86
98
  // Method for defining routes (called when instance is invoked)
87
99
  defineRoute(
88
- route: RouteKey,
89
- generator: Generator,
90
- config: RouteConfig,
100
+ route: Schmock.RouteKey,
101
+ generator: Schmock.Generator,
102
+ config: Schmock.RouteConfig,
91
103
  ): this {
92
104
  // Auto-detect contentType if not provided
93
105
  if (!config.contentType) {
@@ -128,6 +140,17 @@ export class CallableMockInstance {
128
140
  // Parse the route key to create pattern and extract parameters
129
141
  const parsed = parseRouteKey(route);
130
142
 
143
+ // Check for duplicate routes
144
+ const existing = this.routes.find(
145
+ (r) => r.method === parsed.method && r.path === parsed.path,
146
+ );
147
+ if (existing) {
148
+ this.logger.log(
149
+ "warning",
150
+ `Duplicate route: ${route} — first registration wins`,
151
+ );
152
+ }
153
+
131
154
  // Compile the route
132
155
  const compiledRoute: CompiledCallableRoute = {
133
156
  pattern: parsed.pattern,
@@ -139,6 +162,20 @@ export class CallableMockInstance {
139
162
  };
140
163
 
141
164
  this.routes.push(compiledRoute);
165
+
166
+ // Store static routes (no params) in Map for O(1) lookup
167
+ // Only store the first registration — "first registration wins" semantics
168
+ if (parsed.params.length === 0) {
169
+ const normalizedPath =
170
+ parsed.path.endsWith("/") && parsed.path !== "/"
171
+ ? parsed.path.slice(0, -1)
172
+ : parsed.path;
173
+ const key = `${parsed.method} ${normalizedPath}`;
174
+ if (!this.staticRoutes.has(key)) {
175
+ this.staticRoutes.set(key, compiledRoute);
176
+ }
177
+ }
178
+
142
179
  this.logger.log("route", `Route defined: ${route}`, {
143
180
  contentType: config.contentType,
144
181
  generatorType: typeof generator,
@@ -148,7 +185,7 @@ export class CallableMockInstance {
148
185
  return this;
149
186
  }
150
187
 
151
- pipe(plugin: Plugin): this {
188
+ pipe(plugin: Schmock.Plugin): this {
152
189
  this.plugins.push(plugin);
153
190
  this.logger.log(
154
191
  "plugin",
@@ -163,12 +200,83 @@ export class CallableMockInstance {
163
200
  return this;
164
201
  }
165
202
 
203
+ // ===== Request Spy / History API =====
204
+
205
+ history(method?: Schmock.HttpMethod, path?: string): Schmock.RequestRecord[] {
206
+ if (method && path) {
207
+ return this.requestHistory.filter(
208
+ (r) => r.method === method && r.path === path,
209
+ );
210
+ }
211
+ return [...this.requestHistory];
212
+ }
213
+
214
+ called(method?: Schmock.HttpMethod, path?: string): boolean {
215
+ if (method && path) {
216
+ return this.requestHistory.some(
217
+ (r) => r.method === method && r.path === path,
218
+ );
219
+ }
220
+ return this.requestHistory.length > 0;
221
+ }
222
+
223
+ callCount(method?: Schmock.HttpMethod, path?: string): number {
224
+ if (method && path) {
225
+ return this.requestHistory.filter(
226
+ (r) => r.method === method && r.path === path,
227
+ ).length;
228
+ }
229
+ return this.requestHistory.length;
230
+ }
231
+
232
+ lastRequest(
233
+ method?: Schmock.HttpMethod,
234
+ path?: string,
235
+ ): Schmock.RequestRecord | undefined {
236
+ if (method && path) {
237
+ const filtered = this.requestHistory.filter(
238
+ (r) => r.method === method && r.path === path,
239
+ );
240
+ return filtered[filtered.length - 1];
241
+ }
242
+ return this.requestHistory[this.requestHistory.length - 1];
243
+ }
244
+
245
+ // ===== Reset / Lifecycle =====
246
+
247
+ reset(): void {
248
+ this.routes = [];
249
+ this.staticRoutes.clear();
250
+ this.plugins = [];
251
+ this.requestHistory = [];
252
+ if (this.globalConfig.state) {
253
+ for (const key of Object.keys(this.globalConfig.state)) {
254
+ delete this.globalConfig.state[key];
255
+ }
256
+ }
257
+ this.logger.log("lifecycle", "Mock fully reset");
258
+ }
259
+
260
+ resetHistory(): void {
261
+ this.requestHistory = [];
262
+ this.logger.log("lifecycle", "Request history cleared");
263
+ }
264
+
265
+ resetState(): void {
266
+ if (this.globalConfig.state) {
267
+ for (const key of Object.keys(this.globalConfig.state)) {
268
+ delete this.globalConfig.state[key];
269
+ }
270
+ }
271
+ this.logger.log("lifecycle", "State cleared");
272
+ }
273
+
166
274
  async handle(
167
- method: HttpMethod,
275
+ method: Schmock.HttpMethod,
168
276
  path: string,
169
- options?: RequestOptions,
170
- ): Promise<Response> {
171
- const requestId = Math.random().toString(36).substring(7);
277
+ options?: Schmock.RequestOptions,
278
+ ): Promise<Schmock.Response> {
279
+ const requestId = crypto.randomUUID();
172
280
  this.logger.log("request", `[${requestId}] ${method} ${path}`, {
173
281
  headers: options?.headers,
174
282
  query: options?.query,
@@ -179,46 +287,40 @@ export class CallableMockInstance {
179
287
  try {
180
288
  // Apply namespace if configured
181
289
  let requestPath = path;
182
- if (this.globalConfig.namespace) {
183
- // Normalize namespace to handle edge cases
184
- const namespace = this.globalConfig.namespace;
185
- if (namespace === "/") {
186
- // Root namespace means no transformation needed
187
- requestPath = path;
188
- } else {
189
- // Handle namespace without leading slash by normalizing both namespace and path
190
- const normalizedNamespace = namespace.startsWith("/")
191
- ? namespace
192
- : `/${namespace}`;
193
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
194
-
195
- // Remove trailing slash from namespace unless it's root
196
- const finalNamespace =
197
- normalizedNamespace.endsWith("/") && normalizedNamespace !== "/"
198
- ? normalizedNamespace.slice(0, -1)
199
- : normalizedNamespace;
200
-
201
- if (!normalizedPath.startsWith(finalNamespace)) {
202
- this.logger.log(
203
- "route",
204
- `[${requestId}] Path doesn't match namespace ${namespace}`,
205
- );
206
- const error = new RouteNotFoundError(method, path);
207
- const response = {
208
- status: 404,
209
- body: { error: error.message, code: error.code },
210
- headers: {},
211
- };
212
- this.logger.timeEnd(`request-${requestId}`);
213
- return response;
214
- }
290
+ if (this.globalConfig.namespace && this.globalConfig.namespace !== "/") {
291
+ const namespace = this.globalConfig.namespace.startsWith("/")
292
+ ? this.globalConfig.namespace
293
+ : `/${this.globalConfig.namespace}`;
294
+
295
+ const pathToCheck = path.startsWith("/") ? path : `/${path}`;
296
+
297
+ // Check if path starts with namespace
298
+ // handle both "/api/users" (starts with /api) and "/api" (exact match)
299
+ // but NOT "/apiv2" (prefix match but wrong segment)
300
+ const isMatch =
301
+ pathToCheck === namespace ||
302
+ pathToCheck.startsWith(
303
+ namespace.endsWith("/") ? namespace : `${namespace}/`,
304
+ );
215
305
 
216
- // Remove namespace prefix, ensuring we always start with /
217
- requestPath = normalizedPath.substring(finalNamespace.length);
218
- if (!requestPath.startsWith("/")) {
219
- requestPath = `/${requestPath}`;
220
- }
306
+ if (!isMatch) {
307
+ this.logger.log(
308
+ "route",
309
+ `[${requestId}] Path doesn't match namespace ${namespace}`,
310
+ );
311
+ const error = new RouteNotFoundError(method, path);
312
+ const response = {
313
+ status: 404,
314
+ body: { error: error.message, code: error.code },
315
+ headers: {},
316
+ };
317
+ this.logger.timeEnd(`request-${requestId}`);
318
+ return response;
221
319
  }
320
+
321
+ // Remove namespace prefix, ensuring we always start with /
322
+ const stripped = pathToCheck.slice(namespace.length);
323
+ requestPath = stripped.startsWith("/") ? stripped : `/${stripped}`;
222
324
  }
223
325
 
224
326
  // Find matching route
@@ -248,7 +350,7 @@ export class CallableMockInstance {
248
350
  const params = this.extractParams(matchedRoute, requestPath);
249
351
 
250
352
  // Generate initial response from route handler
251
- const context: RequestContext = {
353
+ const context: Schmock.RequestContext = {
252
354
  method,
253
355
  path: requestPath,
254
356
  params,
@@ -258,15 +360,15 @@ export class CallableMockInstance {
258
360
  state: this.globalConfig.state || {},
259
361
  };
260
362
 
261
- let result: any;
262
- if (typeof matchedRoute.generator === "function") {
263
- result = await (matchedRoute.generator as GeneratorFunction)(context);
363
+ let result: unknown;
364
+ if (isGeneratorFunction(matchedRoute.generator)) {
365
+ result = await matchedRoute.generator(context);
264
366
  } else {
265
367
  result = matchedRoute.generator;
266
368
  }
267
369
 
268
370
  // Build plugin context
269
- let pluginContext: PluginContext = {
371
+ let pluginContext: Schmock.PluginContext = {
270
372
  path: requestPath,
271
373
  route: matchedRoute.config,
272
374
  method,
@@ -291,7 +393,7 @@ export class CallableMockInstance {
291
393
  } catch (error) {
292
394
  this.logger.log(
293
395
  "error",
294
- `[${requestId}] Plugin pipeline error: ${(error as Error).message}`,
396
+ `[${requestId}] Plugin pipeline error: ${errorMessage(error)}`,
295
397
  );
296
398
  throw error;
297
399
  }
@@ -302,6 +404,18 @@ export class CallableMockInstance {
302
404
  // Apply global delay if configured
303
405
  await this.applyDelay();
304
406
 
407
+ // Record request in history
408
+ this.requestHistory.push({
409
+ method,
410
+ path: requestPath,
411
+ params,
412
+ query: options?.query || {},
413
+ headers: options?.headers || {},
414
+ body: options?.body,
415
+ timestamp: Date.now(),
416
+ response: { status: response.status, body: response.body },
417
+ });
418
+
305
419
  // Log successful response
306
420
  this.logger.log(
307
421
  "response",
@@ -318,7 +432,7 @@ export class CallableMockInstance {
318
432
  } catch (error) {
319
433
  this.logger.log(
320
434
  "error",
321
- `[${requestId}] Error processing request: ${(error as Error).message}`,
435
+ `[${requestId}] Error processing request: ${errorMessage(error)}`,
322
436
  error,
323
437
  );
324
438
 
@@ -326,11 +440,8 @@ export class CallableMockInstance {
326
440
  const errorResponse = {
327
441
  status: 500,
328
442
  body: {
329
- error: (error as Error).message,
330
- code:
331
- error instanceof SchmockError
332
- ? (error as SchmockError).code
333
- : "INTERNAL_ERROR",
443
+ error: errorMessage(error),
444
+ code: error instanceof SchmockError ? error.code : "INTERNAL_ERROR",
334
445
  },
335
446
  headers: {},
336
447
  };
@@ -371,20 +482,18 @@ export class CallableMockInstance {
371
482
  * @returns Normalized Response object with status, body, and headers
372
483
  * @private
373
484
  */
374
- private parseResponse(result: any, routeConfig: RouteConfig): Response {
485
+ private parseResponse(
486
+ result: unknown,
487
+ routeConfig: Schmock.RouteConfig,
488
+ ): Schmock.Response {
375
489
  let status = 200;
376
- let body = result;
490
+ let body: unknown = result;
377
491
  let headers: Record<string, string> = {};
378
492
 
379
493
  let tupleFormat = false;
380
494
 
381
495
  // Handle already-formed response objects (from plugin error recovery)
382
- if (
383
- result &&
384
- typeof result === "object" &&
385
- "status" in result &&
386
- "body" in result
387
- ) {
496
+ if (isResponseObject(result)) {
388
497
  return {
389
498
  status: result.status,
390
499
  body: result.body,
@@ -441,13 +550,13 @@ export class CallableMockInstance {
441
550
  * @private
442
551
  */
443
552
  private async runPluginPipeline(
444
- context: PluginContext,
445
- initialResponse?: any,
446
- _routeConfig?: RouteConfig,
553
+ context: Schmock.PluginContext,
554
+ initialResponse?: unknown,
555
+ _routeConfig?: Schmock.RouteConfig,
447
556
  _requestId?: string,
448
- ): Promise<{ context: PluginContext; response?: any }> {
557
+ ): Promise<{ context: Schmock.PluginContext; response?: unknown }> {
449
558
  let currentContext = context;
450
- let response: any = initialResponse;
559
+ let response: unknown = initialResponse;
451
560
 
452
561
  this.logger.log(
453
562
  "pipeline",
@@ -486,14 +595,16 @@ export class CallableMockInstance {
486
595
  } catch (error) {
487
596
  this.logger.log(
488
597
  "pipeline",
489
- `Plugin ${plugin.name} failed: ${(error as Error).message}`,
598
+ `Plugin ${plugin.name} failed: ${errorMessage(error)}`,
490
599
  );
491
600
 
492
601
  // Try error handling if plugin has onError hook
493
602
  if (plugin.onError) {
494
603
  try {
604
+ const pluginError =
605
+ error instanceof Error ? error : new Error(errorMessage(error));
495
606
  const errorResult = await plugin.onError(
496
- error as Error,
607
+ pluginError,
497
608
  currentContext,
498
609
  );
499
610
  if (errorResult) {
@@ -501,22 +612,38 @@ export class CallableMockInstance {
501
612
  "pipeline",
502
613
  `Plugin ${plugin.name} handled error`,
503
614
  );
504
- // If error handler returns response, use it and stop pipeline
505
- if (typeof errorResult === "object" && errorResult.status) {
506
- // Return the error response as the current response, stop pipeline
615
+
616
+ // Error return transform the thrown error
617
+ if (errorResult instanceof Error) {
618
+ throw new PluginError(plugin.name, errorResult);
619
+ }
620
+
621
+ // ResponseResult return → recover, stop pipeline
622
+ if (
623
+ typeof errorResult === "object" &&
624
+ errorResult !== null &&
625
+ "status" in errorResult
626
+ ) {
507
627
  response = errorResult;
508
628
  break;
509
629
  }
510
630
  }
631
+ // void/falsy return → propagate original error below
511
632
  } catch (hookError) {
633
+ // If the hook itself threw (including our PluginError above), re-throw it
634
+ if (hookError instanceof PluginError) {
635
+ throw hookError;
636
+ }
512
637
  this.logger.log(
513
638
  "pipeline",
514
- `Plugin ${plugin.name} error handler failed: ${(hookError as Error).message}`,
639
+ `Plugin ${plugin.name} error handler failed: ${errorMessage(hookError)}`,
515
640
  );
516
641
  }
517
642
  }
518
643
 
519
- throw new PluginError(plugin.name, error as Error);
644
+ const cause =
645
+ error instanceof Error ? error : new Error(errorMessage(error));
646
+ throw new PluginError(plugin.name, cause);
520
647
  }
521
648
  }
522
649
 
@@ -533,21 +660,18 @@ export class CallableMockInstance {
533
660
  * @private
534
661
  */
535
662
  private findRoute(
536
- method: HttpMethod,
663
+ method: Schmock.HttpMethod,
537
664
  path: string,
538
665
  ): CompiledCallableRoute | undefined {
539
- // First pass: Look for static routes (routes without parameters)
540
- for (const route of this.routes) {
541
- if (
542
- route.method === method &&
543
- route.params.length === 0 &&
544
- route.pattern.test(path)
545
- ) {
546
- return route;
547
- }
666
+ // O(1) lookup for static routes
667
+ const normalizedPath =
668
+ path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
669
+ const staticMatch = this.staticRoutes.get(`${method} ${normalizedPath}`);
670
+ if (staticMatch) {
671
+ return staticMatch;
548
672
  }
549
673
 
550
- // Second pass: Look for parameterized routes
674
+ // Fall through to parameterized route scan
551
675
  for (const route of this.routes) {
552
676
  if (
553
677
  route.method === method &&
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ HTTP_METHODS,
4
+ isHttpMethod,
5
+ ROUTE_NOT_FOUND_CODE,
6
+ toHttpMethod,
7
+ } from "./constants";
8
+
9
+ describe("constants", () => {
10
+ it("exports ROUTE_NOT_FOUND_CODE", () => {
11
+ expect(ROUTE_NOT_FOUND_CODE).toBe("ROUTE_NOT_FOUND");
12
+ });
13
+
14
+ it("exports all HTTP methods", () => {
15
+ expect(HTTP_METHODS).toEqual([
16
+ "GET",
17
+ "POST",
18
+ "PUT",
19
+ "DELETE",
20
+ "PATCH",
21
+ "HEAD",
22
+ "OPTIONS",
23
+ ]);
24
+ });
25
+ });
26
+
27
+ describe("isHttpMethod", () => {
28
+ it("returns true for valid HTTP methods", () => {
29
+ for (const method of HTTP_METHODS) {
30
+ expect(isHttpMethod(method)).toBe(true);
31
+ }
32
+ });
33
+
34
+ it("returns false for invalid methods", () => {
35
+ expect(isHttpMethod("INVALID")).toBe(false);
36
+ expect(isHttpMethod("")).toBe(false);
37
+ expect(isHttpMethod("get")).toBe(false);
38
+ });
39
+ });
40
+
41
+ describe("toHttpMethod", () => {
42
+ it("converts lowercase to uppercase", () => {
43
+ expect(toHttpMethod("get")).toBe("GET");
44
+ expect(toHttpMethod("post")).toBe("POST");
45
+ expect(toHttpMethod("delete")).toBe("DELETE");
46
+ });
47
+
48
+ it("returns already uppercase methods", () => {
49
+ expect(toHttpMethod("GET")).toBe("GET");
50
+ expect(toHttpMethod("PATCH")).toBe("PATCH");
51
+ });
52
+
53
+ it("throws for invalid methods", () => {
54
+ expect(() => toHttpMethod("INVALID")).toThrow(
55
+ 'Invalid HTTP method: "INVALID"',
56
+ );
57
+ expect(() => toHttpMethod("")).toThrow('Invalid HTTP method: ""');
58
+ });
59
+ });
@@ -0,0 +1,25 @@
1
+ import type { HttpMethod } from "./types.js";
2
+
3
+ export const ROUTE_NOT_FOUND_CODE = "ROUTE_NOT_FOUND" as const;
4
+
5
+ export const HTTP_METHODS: readonly HttpMethod[] = [
6
+ "GET",
7
+ "POST",
8
+ "PUT",
9
+ "DELETE",
10
+ "PATCH",
11
+ "HEAD",
12
+ "OPTIONS",
13
+ ] as const;
14
+
15
+ export function isHttpMethod(method: string): method is HttpMethod {
16
+ return (HTTP_METHODS as readonly string[]).includes(method);
17
+ }
18
+
19
+ export function toHttpMethod(method: string): HttpMethod {
20
+ const upper = method.toUpperCase();
21
+ if (!isHttpMethod(upper)) {
22
+ throw new Error(`Invalid HTTP method: "${method}"`);
23
+ }
24
+ return upper;
25
+ }
package/src/errors.ts CHANGED
@@ -9,7 +9,9 @@ export class SchmockError extends Error {
9
9
  ) {
10
10
  super(message);
11
11
  this.name = "SchmockError";
12
- Error.captureStackTrace(this, this.constructor);
12
+ if (typeof Error.captureStackTrace === "function") {
13
+ Error.captureStackTrace(this, this.constructor);
14
+ }
13
15
  }
14
16
  }
15
17