@schmock/core 1.12.0 → 2.0.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/dist/builder.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare class CallableMockInstance {
13
13
  private callableRef;
14
14
  private server;
15
15
  private serverInfo;
16
+ private interceptHandle;
16
17
  private listeners;
17
18
  constructor(globalConfig?: Schmock.GlobalConfig);
18
19
  defineRoute(route: Schmock.RouteKey, generator: Schmock.Generator, config: Schmock.RouteConfig): this;
@@ -32,6 +33,7 @@ export declare class CallableMockInstance {
32
33
  resetState(): void;
33
34
  listen(port?: number, hostname?: string): Promise<Schmock.ServerInfo>;
34
35
  close(): void;
36
+ intercept(options?: Schmock.InterceptOptions): Schmock.InterceptHandle;
35
37
  handle(method: Schmock.HttpMethod, path: string, options?: Schmock.RequestOptions): Promise<Schmock.Response>;
36
38
  /**
37
39
  * Apply configured response delay
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAuDA;;;;GAIG;AACH,qBAAa,oBAAoB;IAYnB,OAAO,CAAC,YAAY;IAXhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;IAC9D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAiC;IAEnD,OAAO,CAAC,SAAS,CAAoC;gBAEjC,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;IAiFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAS5E,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAS3D,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAS7D,WAAW,CACT,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,GAAG,SAAS;IAYpC,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE;IAQhC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMnC,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAC/B,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAUP,GAAG,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAChC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAKP,OAAO,CAAC,IAAI;IAWZ,KAAK,IAAI,IAAI;IAeb,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IAWlB,MAAM,CAAC,IAAI,SAAI,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;IAqDrE,KAAK,IAAI,IAAI;IAUP,MAAM,CACV,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,GAC/B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;IAoO5B;;;;OAIG;YACW,UAAU;CAezB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAwDA;;;;GAIG;AACH,qBAAa,oBAAoB;IAanB,OAAO,CAAC,YAAY;IAZhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;IAC9D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAiC;IACnD,OAAO,CAAC,eAAe,CAAwC;IAE/D,OAAO,CAAC,SAAS,CAAoC;gBAEjC,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;IAiFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAS5E,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAS3D,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAS7D,WAAW,CACT,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,GAAG,SAAS;IAYpC,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE;IAQhC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMnC,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAC/B,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAUP,GAAG,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAChC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAKP,OAAO,CAAC,IAAI;IAWZ,KAAK,IAAI,IAAI;IAeb,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IAWlB,MAAM,CAAC,IAAI,SAAI,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;IAqDrE,KAAK,IAAI,IAAI;IAYb,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,gBAAgB,GAAG,OAAO,CAAC,eAAe;IAgBhE,MAAM,CACV,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,GAC/B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;IAoO5B;;;;OAIG;YACW,UAAU;CAezB"}
package/dist/builder.js CHANGED
@@ -2,6 +2,7 @@ import { createServer } from "node:http";
2
2
  import { normalizePath, toHttpMethod } from "./constants.js";
3
3
  import { errorMessage, RouteDefinitionError, RouteNotFoundError, SchmockError, } from "./errors.js";
4
4
  import { collectBody, parseNodeHeaders, parseNodeQuery, writeSchmockResponse, } from "./http-helpers.js";
5
+ import { createFetchInterceptor } from "./interceptor.js";
5
6
  import { parseRouteKey } from "./parser.js";
6
7
  import { runPluginPipeline } from "./plugin-pipeline.js";
7
8
  import { parseResponse } from "./response-parser.js";
@@ -52,6 +53,7 @@ export class CallableMockInstance {
52
53
  callableRef;
53
54
  server;
54
55
  serverInfo;
56
+ interceptHandle = null;
55
57
  // biome-ignore lint/complexity/noBannedTypes: internal storage for event listeners with varying signatures
56
58
  listeners = new Map();
57
59
  constructor(globalConfig = {}) {
@@ -284,6 +286,14 @@ export class CallableMockInstance {
284
286
  this.serverInfo = undefined;
285
287
  this.logger.log("server", "Server stopped");
286
288
  }
289
+ // ===== Fetch Interceptor =====
290
+ intercept(options) {
291
+ if (this.interceptHandle?.active) {
292
+ throw new SchmockError("Already intercepting. Call restore() first.", "ALREADY_INTERCEPTING");
293
+ }
294
+ this.interceptHandle = createFetchInterceptor((method, path, opts) => this.handle(method, path, opts), options);
295
+ return this.interceptHandle;
296
+ }
287
297
  async handle(method, path, options) {
288
298
  const handleStart = performance.now();
289
299
  const requestId = this.globalConfig.debug ? crypto.randomUUID() : "";
@@ -5,6 +5,14 @@ export declare function isHttpMethod(method: string): method is HttpMethod;
5
5
  export declare function toHttpMethod(method: string): HttpMethod;
6
6
  export declare function normalizePath(path: string): string;
7
7
  export declare function toRouteKey(method: HttpMethod, path: string): Schmock.RouteKey;
8
+ /**
9
+ * Check if a Schmock response is a route-not-found response.
10
+ * Used by adapters to decide whether to pass through to the real backend.
11
+ */
12
+ export declare function isRouteNotFound(response: {
13
+ status: number;
14
+ body: unknown;
15
+ }): boolean;
8
16
  /**
9
17
  * Check if a value is a status tuple: [status, body] or [status, body, headers]
10
18
  * Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAG7E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQxE"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAG7E;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CASV;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQxE"}
package/dist/constants.js CHANGED
@@ -25,6 +25,18 @@ export function toRouteKey(method, path) {
25
25
  const key = `${method} ${path}`;
26
26
  return key;
27
27
  }
28
+ /**
29
+ * Check if a Schmock response is a route-not-found response.
30
+ * Used by adapters to decide whether to pass through to the real backend.
31
+ */
32
+ export function isRouteNotFound(response) {
33
+ const { status, body } = response;
34
+ return (status === 404 &&
35
+ body !== null &&
36
+ typeof body === "object" &&
37
+ "code" in body &&
38
+ body.code === ROUTE_NOT_FOUND_CODE);
39
+ }
28
40
  /**
29
41
  * Check if a value is a status tuple: [status, body] or [status, body, headers]
30
42
  * Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
@@ -0,0 +1,9 @@
1
+ export declare function notFound(message?: string | object): [number, object];
2
+ export declare function badRequest(message?: string | object): [number, object];
3
+ export declare function unauthorized(message?: string | object): [number, object];
4
+ export declare function forbidden(message?: string | object): [number, object];
5
+ export declare function serverError(message?: string | object): [number, object];
6
+ export declare function created(body: object): [number, object];
7
+ export declare function noContent(): [number, null];
8
+ export declare function paginate<T>(items: T[], options?: Schmock.PaginateOptions): Schmock.PaginatedResponse<T>;
9
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,CACtB,OAAO,GAAE,MAAM,GAAG,MAAoB,GACrC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,UAAU,CACxB,OAAO,GAAE,MAAM,GAAG,MAAsB,GACvC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,YAAY,CAC1B,OAAO,GAAE,MAAM,GAAG,MAAuB,GACxC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,SAAS,CACvB,OAAO,GAAE,MAAM,GAAG,MAAoB,GACrC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,WAAW,CACzB,OAAO,GAAE,MAAM,GAAG,MAAgC,GACjD,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAEtD;AAED,wBAAgB,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAE1C;AAED,wBAAgB,QAAQ,CAAC,CAAC,EACxB,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,GAAE,OAAO,CAAC,eAAoB,GACpC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAS9B"}
@@ -0,0 +1,37 @@
1
+ /// <reference path="../schmock.d.ts" />
2
+ export function notFound(message = "Not Found") {
3
+ const body = typeof message === "string" ? { message } : message;
4
+ return [404, body];
5
+ }
6
+ export function badRequest(message = "Bad Request") {
7
+ const body = typeof message === "string" ? { message } : message;
8
+ return [400, body];
9
+ }
10
+ export function unauthorized(message = "Unauthorized") {
11
+ const body = typeof message === "string" ? { message } : message;
12
+ return [401, body];
13
+ }
14
+ export function forbidden(message = "Forbidden") {
15
+ const body = typeof message === "string" ? { message } : message;
16
+ return [403, body];
17
+ }
18
+ export function serverError(message = "Internal Server Error") {
19
+ const body = typeof message === "string" ? { message } : message;
20
+ return [500, body];
21
+ }
22
+ export function created(body) {
23
+ return [201, body];
24
+ }
25
+ export function noContent() {
26
+ return [204, null];
27
+ }
28
+ export function paginate(items, options = {}) {
29
+ const page = options.page || 1;
30
+ const pageSize = options.pageSize || 10;
31
+ const total = items.length;
32
+ const totalPages = Math.ceil(total / pageSize);
33
+ const start = (page - 1) * pageSize;
34
+ const end = start + pageSize;
35
+ const data = items.slice(start, end);
36
+ return { data, page, pageSize, total, totalPages };
37
+ }
package/dist/index.d.ts CHANGED
@@ -22,8 +22,10 @@
22
22
  * @returns A callable mock instance
23
23
  */
24
24
  export declare function schmock(config?: Schmock.GlobalConfig): Schmock.CallableMockInstance;
25
- export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
25
+ export { HTTP_METHODS, isHttpMethod, isRouteNotFound, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
26
26
  export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
27
+ export { badRequest, created, forbidden, noContent, notFound, paginate, serverError, unauthorized, } from "./helpers.js";
27
28
  export { collectBody, parseNodeHeaders, parseNodeQuery, writeSchmockResponse, } from "./http-helpers.js";
28
- export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, RequestRecord, Response, ResponseBody, ResponseResult, RouteConfig, RouteInfo, RouteKey, ServerInfo, StaticData, } from "./types.js";
29
+ export { createFetchInterceptor } from "./interceptor.js";
30
+ export type { AdapterRequest, AdapterResponse, CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, InterceptHandle, InterceptOptions, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, RequestRecord, Response, ResponseBody, ResponseResult, RouteConfig, RouteInfo, RouteKey, ServerInfo, StaticData, } from "./types.js";
29
31
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CACrB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,GAC5B,OAAO,CAAC,oBAAoB,CAmD9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,YAAY,EACZ,UAAU,GACX,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,oBAAoB,EACpB,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,MAAM,EACN,aAAa,EACb,YAAY,EACZ,cAAc,EACd,cAAc,EACd,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,WAAW,EACX,SAAS,EACT,QAAQ,EACR,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CACrB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,GAC5B,OAAO,CAAC,oBAAoB,CAqD9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,YAAY,EACZ,UAAU,GACX,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,UAAU,EACV,OAAO,EACP,SAAS,EACT,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,YAAY,GACb,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,YAAY,EACV,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,MAAM,EACN,aAAa,EACb,YAAY,EACZ,cAAc,EACd,cAAc,EACd,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,WAAW,EACX,SAAS,EACT,QAAQ,EACR,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -54,13 +54,19 @@ export function schmock(config) {
54
54
  getState: instance.getState.bind(instance),
55
55
  listen: instance.listen.bind(instance),
56
56
  close: instance.close.bind(instance),
57
+ intercept: (options) => instance.intercept(options),
57
58
  });
58
59
  instance.setCallableRef(callableInstance);
59
60
  return callableInstance;
60
61
  }
61
62
  // Re-export constants and utilities
62
- export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
63
+ export { HTTP_METHODS, isHttpMethod, isRouteNotFound, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
63
64
  // Re-export errors
64
65
  export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
66
+ // Re-export response helpers
67
+ export { badRequest, created, forbidden, noContent, notFound, paginate, serverError, unauthorized, } from "./helpers.js";
65
68
  // Re-export HTTP server helpers
66
69
  export { collectBody, parseNodeHeaders, parseNodeQuery, writeSchmockResponse, } from "./http-helpers.js";
70
+ // Re-export types
71
+ // Re-export interceptor
72
+ export { createFetchInterceptor } from "./interceptor.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Create a fetch interceptor that routes requests through mock.handle().
3
+ */
4
+ export declare function createFetchInterceptor(handle: (method: Schmock.HttpMethod, path: string, requestOptions?: Schmock.RequestOptions) => Promise<Schmock.Response>, options?: Schmock.InterceptOptions): Schmock.InterceptHandle;
5
+ //# sourceMappingURL=interceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAwGA;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,CACN,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,KACpC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,EAC9B,OAAO,GAAE,OAAO,CAAC,gBAAqB,GACrC,OAAO,CAAC,eAAe,CAyIzB"}
@@ -0,0 +1,196 @@
1
+ /// <reference path="../schmock.d.ts" />
2
+ import { isRouteNotFound, toHttpMethod } from "./constants.js";
3
+ /**
4
+ * Extract pathname from a URL string, handling both absolute and relative URLs.
5
+ */
6
+ function extractPathname(url) {
7
+ const queryStart = url.indexOf("?");
8
+ const urlWithoutQuery = queryStart === -1 ? url : url.slice(0, queryStart);
9
+ if (urlWithoutQuery.includes("://")) {
10
+ try {
11
+ return new URL(urlWithoutQuery).pathname;
12
+ }
13
+ catch {
14
+ // Fall through to simple extraction
15
+ }
16
+ }
17
+ if (!urlWithoutQuery.startsWith("/")) {
18
+ return `/${urlWithoutQuery}`;
19
+ }
20
+ return urlWithoutQuery;
21
+ }
22
+ /**
23
+ * Extract query parameters from a URL string.
24
+ */
25
+ function extractQuery(url) {
26
+ const queryStart = url.indexOf("?");
27
+ if (queryStart === -1)
28
+ return {};
29
+ const params = new URLSearchParams(url.slice(queryStart + 1));
30
+ const result = {};
31
+ params.forEach((value, key) => {
32
+ result[key] = value;
33
+ });
34
+ return result;
35
+ }
36
+ /**
37
+ * Extract headers from fetch init or Request object.
38
+ */
39
+ function extractHeaders(input, init) {
40
+ const headers = {};
41
+ const raw = input instanceof Request ? input.headers : init?.headers;
42
+ if (!raw)
43
+ return headers;
44
+ if (raw instanceof Headers) {
45
+ raw.forEach((value, key) => {
46
+ headers[key] = value;
47
+ });
48
+ }
49
+ else if (Array.isArray(raw)) {
50
+ for (const [key, value] of raw) {
51
+ headers[key] = value;
52
+ }
53
+ }
54
+ else {
55
+ Object.assign(headers, raw);
56
+ }
57
+ return headers;
58
+ }
59
+ /**
60
+ * Extract body from fetch init, parsing JSON when possible.
61
+ */
62
+ async function extractBody(input, init) {
63
+ const rawBody = input instanceof Request ? input.body : init?.body;
64
+ if (rawBody === null || rawBody === undefined)
65
+ return undefined;
66
+ // String body — try to parse as JSON
67
+ const bodyInit = init?.body;
68
+ if (typeof bodyInit === "string") {
69
+ try {
70
+ return JSON.parse(bodyInit);
71
+ }
72
+ catch {
73
+ return bodyInit;
74
+ }
75
+ }
76
+ // Request with body — clone and read
77
+ if (input instanceof Request && input.body) {
78
+ try {
79
+ return await input.clone().json();
80
+ }
81
+ catch {
82
+ try {
83
+ return await input.clone().text();
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }
89
+ }
90
+ return undefined;
91
+ }
92
+ /**
93
+ * Create a fetch interceptor that routes requests through mock.handle().
94
+ */
95
+ export function createFetchInterceptor(handle, options = {}) {
96
+ const { baseUrl, passthrough = true, beforeRequest, beforeResponse, errorFormatter, } = options;
97
+ const originalFetch = globalThis.fetch;
98
+ let active = true;
99
+ globalThis.fetch = async (input, init) => {
100
+ // Resolve the URL string
101
+ const urlString = input instanceof Request
102
+ ? input.url
103
+ : input instanceof URL
104
+ ? input.href
105
+ : input;
106
+ const path = extractPathname(urlString);
107
+ // BaseUrl filter — non-matching requests go straight to real fetch
108
+ if (baseUrl && !path.startsWith(baseUrl)) {
109
+ return originalFetch(input, init);
110
+ }
111
+ // Build adapter request
112
+ const method = input instanceof Request ? input.method : (init?.method ?? "GET");
113
+ const headers = extractHeaders(input, init);
114
+ const query = extractQuery(urlString);
115
+ const body = await extractBody(input, init);
116
+ let adapterRequest = {
117
+ method,
118
+ path,
119
+ headers,
120
+ body,
121
+ query,
122
+ };
123
+ try {
124
+ // Apply beforeRequest hook
125
+ if (beforeRequest) {
126
+ const modified = await beforeRequest(adapterRequest);
127
+ if (modified) {
128
+ adapterRequest = modified;
129
+ }
130
+ }
131
+ const schmockResponse = await handle(toHttpMethod(adapterRequest.method), adapterRequest.path, {
132
+ headers: adapterRequest.headers,
133
+ body: adapterRequest.body,
134
+ query: adapterRequest.query,
135
+ });
136
+ // Route not found — passthrough or 404
137
+ if (isRouteNotFound(schmockResponse)) {
138
+ if (passthrough) {
139
+ return originalFetch(input, init);
140
+ }
141
+ return new Response(JSON.stringify({
142
+ error: "No matching mock route found",
143
+ code: "ROUTE_NOT_FOUND",
144
+ }), {
145
+ status: 404,
146
+ headers: { "content-type": "application/json" },
147
+ });
148
+ }
149
+ // Apply beforeResponse hook
150
+ let response = schmockResponse;
151
+ if (beforeResponse) {
152
+ const modified = await beforeResponse(response, adapterRequest);
153
+ if (modified) {
154
+ response = modified;
155
+ }
156
+ }
157
+ // Build fetch Response
158
+ const responseHeaders = new Headers(response.headers);
159
+ if (!responseHeaders.has("content-type") &&
160
+ response.body !== null &&
161
+ response.body !== undefined) {
162
+ responseHeaders.set("content-type", "application/json");
163
+ }
164
+ const responseBody = response.body === null || response.body === undefined
165
+ ? null
166
+ : typeof response.body === "string"
167
+ ? response.body
168
+ : JSON.stringify(response.body);
169
+ return new Response(responseBody, {
170
+ status: response.status,
171
+ headers: responseHeaders,
172
+ });
173
+ }
174
+ catch (error) {
175
+ if (errorFormatter) {
176
+ const formatted = errorFormatter(error instanceof Error ? error : new Error(String(error)));
177
+ return new Response(JSON.stringify(formatted), {
178
+ status: 500,
179
+ headers: { "content-type": "application/json" },
180
+ });
181
+ }
182
+ throw error;
183
+ }
184
+ };
185
+ return {
186
+ restore() {
187
+ if (active) {
188
+ globalThis.fetch = originalFetch;
189
+ active = false;
190
+ }
191
+ },
192
+ get active() {
193
+ return active;
194
+ },
195
+ };
196
+ }
@@ -12,7 +12,7 @@ export async function runPluginPipeline(plugins, context, initialResponse, logge
12
12
  logger.log("pipeline", `Processing plugin: ${plugin.name}`);
13
13
  try {
14
14
  const result = await plugin.process(currentContext, response);
15
- if (!result || !result.context) {
15
+ if (!result?.context) {
16
16
  throw new Error(`Plugin ${plugin.name} didn't return valid result`);
17
17
  }
18
18
  currentContext = result.context;
package/dist/types.d.ts CHANGED
@@ -26,4 +26,8 @@ export type SeedSource = Schmock.SeedSource;
26
26
  export type SeedConfig = Schmock.SeedConfig;
27
27
  export type CliOptions = Schmock.CliOptions;
28
28
  export type CliServer = Schmock.CliServer;
29
+ export type AdapterRequest = Schmock.AdapterRequest;
30
+ export type AdapterResponse = Schmock.AdapterResponse;
31
+ export type InterceptOptions = Schmock.InterceptOptions;
32
+ export type InterceptHandle = Schmock.InterceptHandle;
29
33
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;AAC5D,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;AAC5D,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;AACtD,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;AACxD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@schmock/core",
3
3
  "description": "Core functionality for Schmock",
4
- "version": "1.12.0",
4
+ "version": "2.0.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -35,7 +35,7 @@
35
35
  "@amiceli/vitest-cucumber": "^6.3.0",
36
36
  "@types/json-schema": "^7.0.15",
37
37
  "@types/node": "^25.5.0",
38
- "@vitest/ui": "^4.1.1",
39
- "vitest": "^4.1.1"
38
+ "@vitest/ui": "^4.1.2",
39
+ "vitest": "^4.1.2"
40
40
  }
41
41
  }
package/src/builder.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  parseNodeQuery,
14
14
  writeSchmockResponse,
15
15
  } from "./http-helpers.js";
16
+ import { createFetchInterceptor } from "./interceptor.js";
16
17
  import { parseRouteKey } from "./parser.js";
17
18
  import { runPluginPipeline } from "./plugin-pipeline.js";
18
19
  import { parseResponse } from "./response-parser.js";
@@ -67,6 +68,7 @@ export class CallableMockInstance {
67
68
  private callableRef: Schmock.CallableMockInstance | undefined;
68
69
  private server: Server | undefined;
69
70
  private serverInfo: Schmock.ServerInfo | undefined;
71
+ private interceptHandle: Schmock.InterceptHandle | null = null;
70
72
  // biome-ignore lint/complexity/noBannedTypes: internal storage for event listeners with varying signatures
71
73
  private listeners = new Map<string, Set<Function>>();
72
74
 
@@ -374,6 +376,24 @@ export class CallableMockInstance {
374
376
  this.logger.log("server", "Server stopped");
375
377
  }
376
378
 
379
+ // ===== Fetch Interceptor =====
380
+
381
+ intercept(options?: Schmock.InterceptOptions): Schmock.InterceptHandle {
382
+ if (this.interceptHandle?.active) {
383
+ throw new SchmockError(
384
+ "Already intercepting. Call restore() first.",
385
+ "ALREADY_INTERCEPTING",
386
+ );
387
+ }
388
+
389
+ this.interceptHandle = createFetchInterceptor(
390
+ (method, path, opts) => this.handle(method, path, opts),
391
+ options,
392
+ );
393
+
394
+ return this.interceptHandle;
395
+ }
396
+
377
397
  async handle(
378
398
  method: Schmock.HttpMethod,
379
399
  path: string,
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  HTTP_METHODS,
4
4
  isHttpMethod,
5
+ isRouteNotFound,
5
6
  ROUTE_NOT_FOUND_CODE,
6
7
  toHttpMethod,
7
8
  } from "./constants";
@@ -57,3 +58,42 @@ describe("toHttpMethod", () => {
57
58
  expect(() => toHttpMethod("")).toThrow('Invalid HTTP method: ""');
58
59
  });
59
60
  });
61
+
62
+ describe("isRouteNotFound", () => {
63
+ it("returns true for a route-not-found response", () => {
64
+ const response = {
65
+ status: 404,
66
+ body: { error: "Route not found", code: ROUTE_NOT_FOUND_CODE },
67
+ headers: {},
68
+ };
69
+ expect(isRouteNotFound(response)).toBe(true);
70
+ });
71
+
72
+ it("returns false for a regular 404 response", () => {
73
+ const response = {
74
+ status: 404,
75
+ body: { message: "User not found" },
76
+ headers: {},
77
+ };
78
+ expect(isRouteNotFound(response)).toBe(false);
79
+ });
80
+
81
+ it("returns false for a non-404 response", () => {
82
+ const response = {
83
+ status: 200,
84
+ body: { code: ROUTE_NOT_FOUND_CODE },
85
+ headers: {},
86
+ };
87
+ expect(isRouteNotFound(response)).toBe(false);
88
+ });
89
+
90
+ it("returns false when body is null", () => {
91
+ const response = { status: 404, body: null, headers: {} };
92
+ expect(isRouteNotFound(response)).toBe(false);
93
+ });
94
+
95
+ it("returns false when body is a string", () => {
96
+ const response = { status: 404, body: "not found", headers: {} };
97
+ expect(isRouteNotFound(response)).toBe(false);
98
+ });
99
+ });
package/src/constants.ts CHANGED
@@ -33,6 +33,24 @@ export function toRouteKey(method: HttpMethod, path: string): Schmock.RouteKey {
33
33
  return key;
34
34
  }
35
35
 
36
+ /**
37
+ * Check if a Schmock response is a route-not-found response.
38
+ * Used by adapters to decide whether to pass through to the real backend.
39
+ */
40
+ export function isRouteNotFound(response: {
41
+ status: number;
42
+ body: unknown;
43
+ }): boolean {
44
+ const { status, body } = response;
45
+ return (
46
+ status === 404 &&
47
+ body !== null &&
48
+ typeof body === "object" &&
49
+ "code" in body &&
50
+ body.code === ROUTE_NOT_FOUND_CODE
51
+ );
52
+ }
53
+
36
54
  /**
37
55
  * Check if a value is a status tuple: [status, body] or [status, body, headers]
38
56
  * Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.