@schmock/cli 2.0.2 → 2.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 (3) hide show
  1. package/dist/bin.js +74 -36
  2. package/dist/index.js +74 -36
  3. package/package.json +3 -3
package/dist/bin.js CHANGED
@@ -23820,6 +23820,26 @@ function extractPathname(url) {
23820
23820
  }
23821
23821
  return urlWithoutQuery;
23822
23822
  }
23823
+ function extractOrigin(url) {
23824
+ if (!url.includes("://"))
23825
+ return null;
23826
+ try {
23827
+ return new URL(url).origin;
23828
+ } catch {
23829
+ return null;
23830
+ }
23831
+ }
23832
+ function parseBaseUrl(baseUrl) {
23833
+ if (baseUrl.includes("://")) {
23834
+ try {
23835
+ const parsed = new URL(baseUrl);
23836
+ const rawPath = parsed.pathname;
23837
+ const path = rawPath === "/" ? "" : rawPath.replace(/\/$/, "");
23838
+ return { origin: parsed.origin, path };
23839
+ } catch {}
23840
+ }
23841
+ return { origin: null, path: baseUrl.replace(/\/$/, "") };
23842
+ }
23823
23843
  function extractQuery(url) {
23824
23844
  const queryStart = url.indexOf("?");
23825
23845
  if (queryStart === -1)
@@ -23896,10 +23916,18 @@ function createFetchInterceptor(handle, options = {}) {
23896
23916
  const urlString = input instanceof Request ? input.url : input instanceof URL ? input.href : input;
23897
23917
  const path = extractPathname(urlString);
23898
23918
  if (baseUrl) {
23899
- const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
23900
- const isMatch = path === normalizedBase || path.startsWith(`${normalizedBase}/`);
23901
- if (!isMatch) {
23902
- return originalFetch(input, init);
23919
+ const { origin: baseOrigin, path: basePath } = parseBaseUrl(baseUrl);
23920
+ if (baseOrigin) {
23921
+ const reqOrigin = extractOrigin(urlString);
23922
+ if (reqOrigin !== baseOrigin) {
23923
+ return originalFetch(input, init);
23924
+ }
23925
+ }
23926
+ if (basePath) {
23927
+ const isMatch = path === basePath || path.startsWith(`${basePath}/`);
23928
+ if (!isMatch) {
23929
+ return originalFetch(input, init);
23930
+ }
23903
23931
  }
23904
23932
  }
23905
23933
  const method = input instanceof Request ? input.method : init?.method ?? "GET";
@@ -23985,14 +24013,14 @@ function parseRouteKey(routeKey) {
23985
24013
  }
23986
24014
  const [, method, path] = match;
23987
24015
  const params = [];
23988
- const paramPattern = /:([^/]+)/g;
23989
- let paramMatch;
23990
- paramMatch = paramPattern.exec(path);
23991
- while (paramMatch !== null) {
23992
- params.push(paramMatch[1]);
23993
- paramMatch = paramPattern.exec(path);
23994
- }
23995
- const regexPath = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:([^/]+)/g, "([^/]+)");
24016
+ const regexPath = path.split(/(:[a-zA-Z0-9_-]+)/g).map((segment) => {
24017
+ const paramMatch = segment.match(/^:([a-zA-Z0-9_-]+)$/);
24018
+ if (paramMatch) {
24019
+ params.push(paramMatch[1]);
24020
+ return "([^/]+)";
24021
+ }
24022
+ return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24023
+ }).join("");
23996
24024
  const pattern = new RegExp(`^${regexPath}$`);
23997
24025
  return {
23998
24026
  method: toHttpMethod(method),
@@ -24198,6 +24226,7 @@ class CallableMockInstance {
24198
24226
  const existing = this.routes.find((r) => r.method === parsed.method && r.path === parsed.path);
24199
24227
  if (existing) {
24200
24228
  this.logger.log("warning", `Duplicate route: ${route} — first registration wins`);
24229
+ return this;
24201
24230
  }
24202
24231
  const compiledRoute = {
24203
24232
  pattern: parsed.pattern,
@@ -24210,9 +24239,7 @@ class CallableMockInstance {
24210
24239
  this.routes.push(compiledRoute);
24211
24240
  if (parsed.params.length === 0) {
24212
24241
  const key = `${parsed.method} ${normalizePath(parsed.path)}`;
24213
- if (!this.staticRoutes.has(key)) {
24214
- this.staticRoutes.set(key, compiledRoute);
24215
- }
24242
+ this.staticRoutes.set(key, compiledRoute);
24216
24243
  }
24217
24244
  this.logger.log("route", `Route defined: ${route}`, {
24218
24245
  contentType: config.contentType,
@@ -24494,6 +24521,10 @@ class CallableMockInstance {
24494
24521
  timestamp: Date.now(),
24495
24522
  response: { status: response.status, body: response.body }
24496
24523
  });
24524
+ const maxHistorySize = this.globalConfig.maxHistorySize;
24525
+ if (typeof maxHistorySize === "number" && maxHistorySize >= 0 && this.requestHistory.length > maxHistorySize) {
24526
+ this.requestHistory.splice(0, this.requestHistory.length - maxHistorySize);
24527
+ }
24497
24528
  this.emit("request:end", {
24498
24529
  method,
24499
24530
  path: requestPath,
@@ -24576,7 +24607,7 @@ function schmock(config) {
24576
24607
  return callableInstance;
24577
24608
  }
24578
24609
  // ../openapi/package.json
24579
- var version = "2.0.2";
24610
+ var version = "2.1.0";
24580
24611
 
24581
24612
  // ../openapi/src/utils.ts
24582
24613
  function isRecord(value) {
@@ -30005,19 +30036,21 @@ function validateSchema(schema, path = "$") {
30005
30036
  validateSchema(schema.items, `${path}.items`);
30006
30037
  }
30007
30038
  }
30008
- if (hasCircularReference(schema)) {
30009
- throw new SchemaValidationError(path, "Schema contains circular references which are not supported");
30010
- }
30011
- const depth = calculateNestingDepth(schema);
30012
- if (depth > MAX_NESTING_DEPTH) {
30013
- throw new ResourceLimitError("schema_nesting_depth", MAX_NESTING_DEPTH, depth);
30014
- }
30015
- if (depth >= DEEP_NESTING_THRESHOLD) {
30016
- checkForDeepNestingWithArrays(schema, path);
30017
- }
30018
- checkArraySizeLimits(schema, path);
30019
- if (schema.$ref === "#") {
30020
- throw new SchemaValidationError(path, "Self-referencing schemas are not supported");
30039
+ if (path === "$") {
30040
+ if (hasCircularReference(schema)) {
30041
+ throw new SchemaValidationError(path, "Schema contains circular references which are not supported");
30042
+ }
30043
+ if (schema.$ref === "#") {
30044
+ throw new SchemaValidationError(path, "Self-referencing schemas are not supported");
30045
+ }
30046
+ const depth = calculateNestingDepth(schema);
30047
+ if (depth > MAX_NESTING_DEPTH) {
30048
+ throw new ResourceLimitError("schema_nesting_depth", MAX_NESTING_DEPTH, depth);
30049
+ }
30050
+ if (depth >= DEEP_NESTING_THRESHOLD) {
30051
+ checkForDeepNestingWithArrays(schema, path);
30052
+ }
30053
+ checkArraySizeLimits(schema, path);
30021
30054
  }
30022
30055
  }
30023
30056
  function hasCircularReference(schema, currentPath = new Set) {
@@ -31636,17 +31669,21 @@ function processContentNegotiation(context) {
31636
31669
  }
31637
31670
  return;
31638
31671
  }
31639
- var ajv = new import_ajv.default({ allErrors: true });
31640
- var schemaCache = new WeakMap;
31641
- function validateRequestBody(context) {
31672
+ function createBodyValidatorContext() {
31673
+ return {
31674
+ ajv: new import_ajv.default({ allErrors: true }),
31675
+ cache: new WeakMap
31676
+ };
31677
+ }
31678
+ function validateRequestBody(context, validatorCtx) {
31642
31679
  const requestBodySchema = getRouteRequestBody(context.route);
31643
31680
  if (!requestBodySchema || context.body === undefined) {
31644
31681
  return;
31645
31682
  }
31646
- let validate = schemaCache.get(requestBodySchema);
31683
+ let validate = validatorCtx.cache.get(requestBodySchema);
31647
31684
  if (!validate) {
31648
- validate = ajv.compile(requestBodySchema);
31649
- schemaCache.set(requestBodySchema, validate);
31685
+ validate = validatorCtx.ajv.compile(requestBodySchema);
31686
+ validatorCtx.cache.set(requestBodySchema, validate);
31650
31687
  }
31651
31688
  if (!validate(context.body)) {
31652
31689
  const errors = validate.errors?.map((e3) => ({
@@ -31766,6 +31803,7 @@ async function openapi(options) {
31766
31803
  }
31767
31804
  const securitySchemes = spec.securitySchemes;
31768
31805
  const globalSecurity = spec.globalSecurity;
31806
+ const bodyValidatorCtx = createBodyValidatorContext();
31769
31807
  return {
31770
31808
  name: "@schmock/openapi",
31771
31809
  version,
@@ -31827,7 +31865,7 @@ async function openapi(options) {
31827
31865
  if (contentResult)
31828
31866
  return contentResult;
31829
31867
  if (options.validateRequests) {
31830
- const validationResult = validateRequestBody(context);
31868
+ const validationResult = validateRequestBody(context, bodyValidatorCtx);
31831
31869
  if (validationResult) {
31832
31870
  return validationResult;
31833
31871
  }
package/dist/index.js CHANGED
@@ -23819,6 +23819,26 @@ function extractPathname(url) {
23819
23819
  }
23820
23820
  return urlWithoutQuery;
23821
23821
  }
23822
+ function extractOrigin(url) {
23823
+ if (!url.includes("://"))
23824
+ return null;
23825
+ try {
23826
+ return new URL(url).origin;
23827
+ } catch {
23828
+ return null;
23829
+ }
23830
+ }
23831
+ function parseBaseUrl(baseUrl) {
23832
+ if (baseUrl.includes("://")) {
23833
+ try {
23834
+ const parsed = new URL(baseUrl);
23835
+ const rawPath = parsed.pathname;
23836
+ const path = rawPath === "/" ? "" : rawPath.replace(/\/$/, "");
23837
+ return { origin: parsed.origin, path };
23838
+ } catch {}
23839
+ }
23840
+ return { origin: null, path: baseUrl.replace(/\/$/, "") };
23841
+ }
23822
23842
  function extractQuery(url) {
23823
23843
  const queryStart = url.indexOf("?");
23824
23844
  if (queryStart === -1)
@@ -23895,10 +23915,18 @@ function createFetchInterceptor(handle, options = {}) {
23895
23915
  const urlString = input instanceof Request ? input.url : input instanceof URL ? input.href : input;
23896
23916
  const path = extractPathname(urlString);
23897
23917
  if (baseUrl) {
23898
- const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
23899
- const isMatch = path === normalizedBase || path.startsWith(`${normalizedBase}/`);
23900
- if (!isMatch) {
23901
- return originalFetch(input, init);
23918
+ const { origin: baseOrigin, path: basePath } = parseBaseUrl(baseUrl);
23919
+ if (baseOrigin) {
23920
+ const reqOrigin = extractOrigin(urlString);
23921
+ if (reqOrigin !== baseOrigin) {
23922
+ return originalFetch(input, init);
23923
+ }
23924
+ }
23925
+ if (basePath) {
23926
+ const isMatch = path === basePath || path.startsWith(`${basePath}/`);
23927
+ if (!isMatch) {
23928
+ return originalFetch(input, init);
23929
+ }
23902
23930
  }
23903
23931
  }
23904
23932
  const method = input instanceof Request ? input.method : init?.method ?? "GET";
@@ -23984,14 +24012,14 @@ function parseRouteKey(routeKey) {
23984
24012
  }
23985
24013
  const [, method, path] = match;
23986
24014
  const params = [];
23987
- const paramPattern = /:([^/]+)/g;
23988
- let paramMatch;
23989
- paramMatch = paramPattern.exec(path);
23990
- while (paramMatch !== null) {
23991
- params.push(paramMatch[1]);
23992
- paramMatch = paramPattern.exec(path);
23993
- }
23994
- const regexPath = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:([^/]+)/g, "([^/]+)");
24015
+ const regexPath = path.split(/(:[a-zA-Z0-9_-]+)/g).map((segment) => {
24016
+ const paramMatch = segment.match(/^:([a-zA-Z0-9_-]+)$/);
24017
+ if (paramMatch) {
24018
+ params.push(paramMatch[1]);
24019
+ return "([^/]+)";
24020
+ }
24021
+ return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24022
+ }).join("");
23995
24023
  const pattern = new RegExp(`^${regexPath}$`);
23996
24024
  return {
23997
24025
  method: toHttpMethod(method),
@@ -24197,6 +24225,7 @@ class CallableMockInstance {
24197
24225
  const existing = this.routes.find((r) => r.method === parsed.method && r.path === parsed.path);
24198
24226
  if (existing) {
24199
24227
  this.logger.log("warning", `Duplicate route: ${route} — first registration wins`);
24228
+ return this;
24200
24229
  }
24201
24230
  const compiledRoute = {
24202
24231
  pattern: parsed.pattern,
@@ -24209,9 +24238,7 @@ class CallableMockInstance {
24209
24238
  this.routes.push(compiledRoute);
24210
24239
  if (parsed.params.length === 0) {
24211
24240
  const key = `${parsed.method} ${normalizePath(parsed.path)}`;
24212
- if (!this.staticRoutes.has(key)) {
24213
- this.staticRoutes.set(key, compiledRoute);
24214
- }
24241
+ this.staticRoutes.set(key, compiledRoute);
24215
24242
  }
24216
24243
  this.logger.log("route", `Route defined: ${route}`, {
24217
24244
  contentType: config.contentType,
@@ -24493,6 +24520,10 @@ class CallableMockInstance {
24493
24520
  timestamp: Date.now(),
24494
24521
  response: { status: response.status, body: response.body }
24495
24522
  });
24523
+ const maxHistorySize = this.globalConfig.maxHistorySize;
24524
+ if (typeof maxHistorySize === "number" && maxHistorySize >= 0 && this.requestHistory.length > maxHistorySize) {
24525
+ this.requestHistory.splice(0, this.requestHistory.length - maxHistorySize);
24526
+ }
24496
24527
  this.emit("request:end", {
24497
24528
  method,
24498
24529
  path: requestPath,
@@ -24575,7 +24606,7 @@ function schmock(config) {
24575
24606
  return callableInstance;
24576
24607
  }
24577
24608
  // ../openapi/package.json
24578
- var version = "2.0.2";
24609
+ var version = "2.1.0";
24579
24610
 
24580
24611
  // ../openapi/src/utils.ts
24581
24612
  function isRecord(value) {
@@ -30004,19 +30035,21 @@ function validateSchema(schema, path = "$") {
30004
30035
  validateSchema(schema.items, `${path}.items`);
30005
30036
  }
30006
30037
  }
30007
- if (hasCircularReference(schema)) {
30008
- throw new SchemaValidationError(path, "Schema contains circular references which are not supported");
30009
- }
30010
- const depth = calculateNestingDepth(schema);
30011
- if (depth > MAX_NESTING_DEPTH) {
30012
- throw new ResourceLimitError("schema_nesting_depth", MAX_NESTING_DEPTH, depth);
30013
- }
30014
- if (depth >= DEEP_NESTING_THRESHOLD) {
30015
- checkForDeepNestingWithArrays(schema, path);
30016
- }
30017
- checkArraySizeLimits(schema, path);
30018
- if (schema.$ref === "#") {
30019
- throw new SchemaValidationError(path, "Self-referencing schemas are not supported");
30038
+ if (path === "$") {
30039
+ if (hasCircularReference(schema)) {
30040
+ throw new SchemaValidationError(path, "Schema contains circular references which are not supported");
30041
+ }
30042
+ if (schema.$ref === "#") {
30043
+ throw new SchemaValidationError(path, "Self-referencing schemas are not supported");
30044
+ }
30045
+ const depth = calculateNestingDepth(schema);
30046
+ if (depth > MAX_NESTING_DEPTH) {
30047
+ throw new ResourceLimitError("schema_nesting_depth", MAX_NESTING_DEPTH, depth);
30048
+ }
30049
+ if (depth >= DEEP_NESTING_THRESHOLD) {
30050
+ checkForDeepNestingWithArrays(schema, path);
30051
+ }
30052
+ checkArraySizeLimits(schema, path);
30020
30053
  }
30021
30054
  }
30022
30055
  function hasCircularReference(schema, currentPath = new Set) {
@@ -31635,17 +31668,21 @@ function processContentNegotiation(context) {
31635
31668
  }
31636
31669
  return;
31637
31670
  }
31638
- var ajv = new import_ajv.default({ allErrors: true });
31639
- var schemaCache = new WeakMap;
31640
- function validateRequestBody(context) {
31671
+ function createBodyValidatorContext() {
31672
+ return {
31673
+ ajv: new import_ajv.default({ allErrors: true }),
31674
+ cache: new WeakMap
31675
+ };
31676
+ }
31677
+ function validateRequestBody(context, validatorCtx) {
31641
31678
  const requestBodySchema = getRouteRequestBody(context.route);
31642
31679
  if (!requestBodySchema || context.body === undefined) {
31643
31680
  return;
31644
31681
  }
31645
- let validate = schemaCache.get(requestBodySchema);
31682
+ let validate = validatorCtx.cache.get(requestBodySchema);
31646
31683
  if (!validate) {
31647
- validate = ajv.compile(requestBodySchema);
31648
- schemaCache.set(requestBodySchema, validate);
31684
+ validate = validatorCtx.ajv.compile(requestBodySchema);
31685
+ validatorCtx.cache.set(requestBodySchema, validate);
31649
31686
  }
31650
31687
  if (!validate(context.body)) {
31651
31688
  const errors = validate.errors?.map((e3) => ({
@@ -31765,6 +31802,7 @@ async function openapi(options) {
31765
31802
  }
31766
31803
  const securitySchemes = spec.securitySchemes;
31767
31804
  const globalSecurity = spec.globalSecurity;
31805
+ const bodyValidatorCtx = createBodyValidatorContext();
31768
31806
  return {
31769
31807
  name: "@schmock/openapi",
31770
31808
  version,
@@ -31826,7 +31864,7 @@ async function openapi(options) {
31826
31864
  if (contentResult)
31827
31865
  return contentResult;
31828
31866
  if (options.validateRequests) {
31829
- const validationResult = validateRequestBody(context);
31867
+ const validationResult = validateRequestBody(context, bodyValidatorCtx);
31830
31868
  if (validationResult) {
31831
31869
  return validationResult;
31832
31870
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@schmock/cli",
3
3
  "description": "CLI for running Schmock mock servers from OpenAPI specs",
4
- "version": "2.0.2",
4
+ "version": "2.1.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
- "@schmock/core": "^2.0.2",
36
- "@schmock/openapi": "^2.0.2"
35
+ "@schmock/core": "^2.1.0",
36
+ "@schmock/openapi": "^2.1.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@amiceli/vitest-cucumber": "^6.5.0",