Repository: miragejs/miragejs
Pull request #1036 (2022-04-23 13:19:13)

Extract pretender code to its own module for future move to another package
This PR moves all the pretender related code to the file `mock-server/pretender-config` and conditions the creation of pretender on the presence of an additional server option `interceptor`.

The goal of this PR was to be fully backward compatible. It opens up the ability to replace the interceptor with a different interceptor or allow for no interceptor at all.

First step towards https://github.com/miragejs/miragejs/issues/1013

IanVS: Thanks for this work, @cah-briangantzler!  I'm not terribly familiar with the pretender/mirage codebase (I mostly hopped in as a maintainer to keep dependencies and types up to date), but I really appreciate the work you're doing here, and I think it's a move in the right direction.  I'd like to ask @samselikoff if he has any concerns with the process you've outlined in https://github.com/miragejs/miragejs/issues/1037, and if not, I say let's go for it.  I'll give this a review as soon as I can, but I'm a bit swamped at the moment, so please ping me later this week if you haven't heard anything back.
cah-briangantzler: Thanks for the feedback. Yea, there definitely room for improvement but I wanted to open the ability to experiment (I already have MSW mostly working) without being a breaking change and this does that. There will be breaking in the future, trying to keep that to a minimum  

Patch:
diff --git a/lib/index.js b/lib/index.js
index cc118c6..0c134fa 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -3,7 +3,7 @@ import IdentityManager from "./identity-manager";
 import association from "./association";
 import trait from "./trait";
 import Response from "./response";
-import Server, { createServer, defaultPassthroughs } from "./server";
+import Server, { createServer } from "./server";
 import Model from "./orm/model";
 import Collection from "./orm/collection";
 import Serializer from "./serializer";
@@ -47,6 +47,8 @@ import _utilsIsAssociation from "./utils/is-association";
 import _utilsReferenceSort from "./utils/reference-sort";
 import _utilsUuid from "./utils/uuid";
 
+import PretenderInterceptor from "./mock-server/pretender-config";
+
 /**
   @hide
 */
@@ -72,7 +74,6 @@ export {
   RestSerializer,
   hasMany,
   belongsTo,
-  defaultPassthroughs,
   createServer,
   Server,
   Factory,
@@ -105,6 +106,7 @@ export {
   _utilsIsAssociation,
   _utilsReferenceSort,
   _utilsUuid,
+  PretenderInterceptor,
 };
 
 export default {
diff --git a/lib/mock-server/pretender-config.js b/lib/mock-server/pretender-config.js
new file mode 100644
index 0000000..798b957
--- /dev/null
+++ b/lib/mock-server/pretender-config.js
@@ -0,0 +1,524 @@
+import "@miragejs/pretender-node-polyfill/before";
+import Pretender from "pretender";
+import "@miragejs/pretender-node-polyfill/after";
+import assert from "../assert";
+
+/**
+  Mirage Interceptor Class
+
+    urlPrefix;
+
+    namespace;
+
+    // Creates the interceptor instance
+    constructor(mirageServer, mirageConfig)
+
+    // Allow you to change some of the config options after the server is created
+    config(mirageConfig)
+
+    // These are the equivalent of the functions that were on the Mirage Server.
+    // Those Mirage Server functions are redirected to the Interceptors functions for
+    // backward compatibility
+    get
+    post
+    put
+    delete
+    del
+    patch
+    head
+    options
+
+    // Start the interceptor. (Optional) this happens after the mirage server has been completed configured
+    // and all the models, routes, etc have been defined.
+    start
+    // Shutdown the interceptor instance
+    shutdown
+
+ */
+
+/**
+ @hide
+ */
+const defaultPassthroughs = [
+  "http://localhost:0/chromecheckurl", // mobile chrome
+  "http://localhost:30820/socket.io", // electron
+  (request) => {
+    return /.+\.hot-update.json$/.test(request.url);
+  },
+];
+
+/**
+ @hide
+ */
+export { defaultPassthroughs };
+
+export default class PretenderConfig {
+  urlPrefix;
+
+  namespace;
+
+  timing;
+
+  passthroughChecks;
+
+  pretender;
+
+  mirageServer;
+
+  create(mirageServer, config) {
+    this.mirageServer = mirageServer;
+    this.pretender = this._create(mirageServer);
+
+    /**
+     Mirage uses [pretender.js](https://github.com/trek/pretender) as its xhttp interceptor. In your Mirage config, `this.pretender` refers to the actual Pretender instance, so any config options that work there will work here as well.
+
+     ```js
+     createServer({
+        routes() {
+          this.pretender.handledRequest = (verb, path, request) => {
+            console.log(`Your server responded to ${path}`);
+          }
+        }
+      })
+     ```
+
+     Refer to [Pretender's docs](https://github.com/pretenderjs/pretender) if you want to change any options on your Pretender instance.
+
+     @property pretender
+     @return {Object} The Pretender instance
+     @public
+     */
+    mirageServer.pretender = this.pretender;
+
+    this.passthroughChecks = this.passthroughChecks || [];
+
+    this.config(config);
+
+    [
+      ["get"],
+      ["post"],
+      ["put"],
+      ["delete", "del"],
+      ["patch"],
+      ["head"],
+      ["options"],
+    ].forEach(([verb, alias]) => {
+      this[verb] = (path, ...args) => {
+        let handler = mirageServer.registerRouteHandler(verb, path, args);
+        let fullPath = this._getFullPath(path);
+        let timing =
+          config.timing !== undefined ? config.timing : () => this.timing;
+        return this.pretender?.[verb](fullPath, handler, timing);
+      };
+
+      mirageServer[verb] = this[verb];
+      if (alias) {
+        this[alias] = this[verb];
+        mirageServer[alias] = this[verb];
+      }
+    });
+  }
+
+  config(config) {
+    let useDefaultPassthroughs =
+      typeof config.useDefaultPassthroughs !== "undefined"
+        ? config.useDefaultPassthroughs
+        : true;
+    if (useDefaultPassthroughs) {
+      this._configureDefaultPassthroughs();
+    }
+
+    let didOverridePretenderConfig =
+      config.trackRequests !== undefined && this.pretender;
+    assert(
+      !didOverridePretenderConfig,
+      "You cannot modify Pretender's request tracking once the server is created"
+    );
+
+    /**
+     Set the number of milliseconds for the the Server's response time.
+
+     By default there's a 400ms delay during development, and 0 delay in testing (so your tests run fast).
+
+     ```js
+     createServer({
+        routes() {
+          this.timing = 400; // default
+        }
+      })
+     ```
+
+     To set the timing for individual routes, see the `timing` option for route handlers.
+
+     @property timing
+     @type Number
+     @public
+     */
+    this.timing = this.timing || config.timing || 400;
+
+    /**
+     Sets a string to prefix all route handler URLs with.
+
+     Useful if your app makes API requests to a different port.
+
+     ```js
+     createServer({
+        routes() {
+          this.urlPrefix = 'http://localhost:8080'
+        }
+      })
+     ```
+     */
+    this.urlPrefix = this.urlPrefix || config.urlPrefix || "";
+
+    /**
+     Set the base namespace used for all routes defined with `get`, `post`, `put` or `del`.
+
+     For example,
+
+     ```js
+     createServer({
+        routes() {
+          this.namespace = '/api';
+
+          // this route will handle the URL '/api/contacts'
+          this.get('/contacts', 'contacts');
+        }
+      })
+     ```
+
+     Note that only routes defined after `this.namespace` are affected. This is useful if you have a few one-off routes that you don't want under your namespace:
+
+     ```js
+     createServer({
+        routes() {
+
+          // this route handles /auth
+          this.get('/auth', function() { ...});
+
+          this.namespace = '/api';
+          // this route will handle the URL '/api/contacts'
+          this.get('/contacts', 'contacts');
+        };
+      })
+     ```
+
+     If your app is loaded from the filesystem vs. a server (e.g. via Cordova or Electron vs. `localhost` or `https://yourhost.com/`), you will need to explicitly define a namespace. Likely values are `/` (if requests are made with relative paths) or `https://yourhost.com/api/...` (if requests are made to a defined server).
+
+     For a sample implementation leveraging a configured API host & namespace, check out [this issue comment](https://github.com/miragejs/ember-cli-mirage/issues/497#issuecomment-183458721).
+
+     @property namespace
+     @type String
+     @public
+     */
+    this.namespace = this.namespace || config.namespace || "";
+  }
+
+  /**
+   *
+   * @private
+   * @hide
+   */
+  _configureDefaultPassthroughs() {
+    defaultPassthroughs.forEach((passthroughUrl) => {
+      this.passthrough(passthroughUrl);
+    });
+  }
+
+  /**
+   * Creates a new Pretender instance.
+   *
+   * @method _create
+   * @param {Server} server
+   * @return {Object} A new Pretender instance.
+   * @public
+   */
+  _create(mirageServer) {
+    if (typeof window !== "undefined") {
+      return new Pretender(
+        function () {
+          this.passthroughRequest = function (verb, path, request) {
+            if (mirageServer.shouldLog()) {
+              console.log(
+                `Mirage: Passthrough request for ${verb.toUpperCase()} ${
+                  request.url
+                }`
+              );
+            }
+          };
+
+          this.handledRequest = function (verb, path, request) {
+            if (mirageServer.shouldLog()) {
+              console.groupCollapsed(
+                `Mirage: [${request.status}] ${verb.toUpperCase()} ${
+                  request.url
+                }`
+              );
+              let { requestBody, responseText } = request;
+              let loggedRequest, loggedResponse;
+
+              try {
+                loggedRequest = JSON.parse(requestBody);
+              } catch (e) {
+                loggedRequest = requestBody;
+              }
+
+              try {
+                loggedResponse = JSON.parse(responseText);
+              } catch (e) {
+                loggedResponse = responseText;
+              }
+
+              console.groupCollapsed("Response");
+              console.log(loggedResponse);
+              console.groupEnd();
+
+              console.groupCollapsed("Request (data)");
+              console.log(loggedRequest);
+              console.groupEnd();
+
+              console.groupCollapsed("Request (raw)");
+              console.log(request);
+              console.groupEnd();
+
+              console.groupEnd();
+            }
+          };
+
+          let originalCheckPassthrough = this.checkPassthrough;
+          this.checkPassthrough = function (request) {
+            let shouldPassthrough = mirageServer.passthroughChecks.some(
+              (passthroughCheck) => passthroughCheck(request)
+            );
+
+            if (shouldPassthrough) {
+              let url = request.url.includes("?")
+                ? request.url.substr(0, request.url.indexOf("?"))
+                : request.url;
+
+              this[request.method.toLowerCase()](url, this.passthrough);
+            }
+
+            return originalCheckPassthrough.apply(this, arguments);
+          };
+
+          this.unhandledRequest = function (verb, path) {
+            path = decodeURI(path);
+            assert(
+              `Your app tried to ${verb} '${path}', but there was no route defined to handle this request. Define a route for this endpoint in your routes() config. Did you forget to define a namespace?`
+            );
+          };
+        },
+        { trackRequests: mirageServer.shouldTrackRequests() }
+      );
+    }
+  }
+
+  /**
+   By default, if your app makes a request that is not defined in your server config, Mirage will throw an error. You can use `passthrough` to whitelist requests, and allow them to pass through your Mirage server to the actual network layer.
+
+   Note: Put all passthrough config at the bottom of your routes, to give your route handlers precedence.
+
+   To ignore paths on your current host (as well as configured `namespace`), use a leading `/`:
+
+   ```js
+   this.passthrough('/addresses');
+   ```
+
+   You can also pass a list of paths, or call `passthrough` multiple times:
+
+   ```js
+   this.passthrough('/addresses', '/contacts');
+   this.passthrough('/something');
+   this.passthrough('/else');
+   ```
+
+   These lines will allow all HTTP verbs to pass through. If you want only certain verbs to pass through, pass an array as the last argument with the specified verbs:
+
+   ```js
+   this.passthrough('/addresses', ['post']);
+   this.passthrough('/contacts', '/photos', ['get']);
+   ```
+
+   You can pass a function to `passthrough` to do a runtime check on whether or not the request should be handled by Mirage. If the function returns `true` Mirage will not handle the request and let it pass through.
+
+   ```js
+   this.passthrough(request => {
+      return request.queryParams.skipMirage;
+    });
+   ```
+
+   If you want all requests on the current domain to pass through, simply invoke the method with no arguments:
+
+   ```js
+   this.passthrough();
+   ```
+
+   Note again that the current namespace (i.e. any `namespace` property defined above this call) will be applied.
+
+   You can also allow other-origin hosts to passthrough. If you use a fully-qualified domain name, the `namespace` property will be ignored. Use two * wildcards to match all requests under a path:
+
+   ```js
+   this.passthrough('http://api.foo.bar/**');
+   this.passthrough('http://api.twitter.com/v1/cards/**');
+   ```
+
+   In versions of Pretender prior to 0.12, `passthrough` only worked with jQuery >= 2.x. As long as you're on Pretender@0.12 or higher, you should be all set.
+
+   @method passthrough
+   @param {String} [...paths] Any number of paths to whitelist
+   @param {Array} options Unused
+   @public
+   */
+  passthrough(...paths) {
+    // this only works in browser-like environments for now. in node users will have to configure
+    // their own interceptor if they are using one.
+    if (typeof window !== "undefined") {
+      let verbs = ["get", "post", "put", "delete", "patch", "options", "head"];
+      let lastArg = paths[paths.length - 1];
+
+      if (paths.length === 0) {
+        paths = ["/**", "/"];
+      } else if (Array.isArray(lastArg)) {
+        verbs = paths.pop();
+      }
+
+      paths.forEach((path) => {
+        if (typeof path === "function") {
+          this.passthroughChecks.push(path);
+        } else {
+          verbs.forEach((verb) => {
+            let fullPath = this._getFullPath(path);
+            this.pretender[verb](fullPath, this.pretender.passthrough);
+          });
+        }
+      });
+    }
+  }
+
+  /**
+   * Builds a full path for Pretender to monitor based on the `path` and
+   * configured options (`urlPrefix` and `namespace`).
+   *
+   * @private
+   * @hide
+   */
+  _getFullPath(path) {
+    path = path[0] === "/" ? path.slice(1) : path;
+    let fullPath = "";
+    let urlPrefix = this.urlPrefix ? this.urlPrefix.trim() : "";
+    let namespace = "";
+
+    // if there is a urlPrefix and a namespace
+    if (this.urlPrefix && this.namespace) {
+      if (
+        this.namespace[0] === "/" &&
+        this.namespace[this.namespace.length - 1] === "/"
+      ) {
+        namespace = this.namespace
+          .substring(0, this.namespace.length - 1)
+          .substring(1);
+      }
+
+      if (
+        this.namespace[0] === "/" &&
+        this.namespace[this.namespace.length - 1] !== "/"
+      ) {
+        namespace = this.namespace.substring(1);
+      }
+
+      if (
+        this.namespace[0] !== "/" &&
+        this.namespace[this.namespace.length - 1] === "/"
+      ) {
+        namespace = this.namespace.substring(0, this.namespace.length - 1);
+      }
+
+      if (
+        this.namespace[0] !== "/" &&
+        this.namespace[this.namespace.length - 1] !== "/"
+      ) {
+        namespace = this.namespace;
+      }
+    }
+
+    // if there is a namespace and no urlPrefix
+    if (this.namespace && !this.urlPrefix) {
+      if (
+        this.namespace[0] === "/" &&
+        this.namespace[this.namespace.length - 1] === "/"
+      ) {
+        namespace = this.namespace.substring(0, this.namespace.length - 1);
+      }
+
+      if (
+        this.namespace[0] === "/" &&
+        this.namespace[this.namespace.length - 1] !== "/"
+      ) {
+        namespace = this.namespace;
+      }
+
+      if (
+        this.namespace[0] !== "/" &&
+        this.namespace[this.namespace.length - 1] === "/"
+      ) {
+        let namespaceSub = this.namespace.substring(
+          0,
+          this.namespace.length - 1
+        );
+        namespace = `/${namespaceSub}`;
+      }
+
+      if (
+        this.namespace[0] !== "/" &&
+        this.namespace[this.namespace.length - 1] !== "/"
+      ) {
+        namespace = `/${this.namespace}`;
+      }
+    }
+
+    // if no namespace
+    if (!this.namespace) {
+      namespace = "";
+    }
+
+    // check to see if path is a FQDN. if so, ignore any urlPrefix/namespace that was set
+    if (/^https?:\/\//.test(path)) {
+      fullPath += path;
+    } else {
+      // otherwise, if there is a urlPrefix, use that as the beginning of the path
+      if (urlPrefix.length) {
+        fullPath +=
+          urlPrefix[urlPrefix.length - 1] === "/" ? urlPrefix : `${urlPrefix}/`;
+      }
+
+      // add the namespace to the path
+      fullPath += namespace;
+
+      // add a trailing slash to the path if it doesn't already contain one
+      if (fullPath[fullPath.length - 1] !== "/") {
+        fullPath += "/";
+      }
+
+      // finally add the configured path
+      fullPath += path;
+
+      // if we're making a same-origin request, ensure a / is prepended and
+      // dedup any double slashes
+      if (!/^https?:\/\//.test(fullPath)) {
+        fullPath = `/${fullPath}`;
+        fullPath = fullPath.replace(/\/+/g, "/");
+      }
+    }
+
+    return fullPath;
+  }
+
+  start() {
+    // unneeded for pretender implementation
+  }
+
+  shutdown() {
+    this.pretender.shutdown();
+  }
+}
diff --git a/lib/server.js b/lib/server.js
index 1a7406f..5dd2e02 100644
--- a/lib/server.js
+++ b/lib/server.js
@@ -1,8 +1,5 @@
 /* eslint no-console: 0 */
 
-import "@miragejs/pretender-node-polyfill/before";
-import Pretender from "pretender";
-import "@miragejs/pretender-node-polyfill/after";
 import { camelize } from "./utils/inflector";
 import isAssociation from "./utils/is-association";
 import assert from "./assert";
@@ -15,94 +12,9 @@ import find from "lodash.find";
 import isPlainObject from "lodash.isplainobject";
 import isInteger from "lodash.isinteger";
 
-const isPluralForModelCache = {};
-
-/**
- * Creates a new Pretender instance.
- *
- * @method createPretender
- * @param {Server} server
- * @return {Object} A new Pretender instance.
- * @public
- */
-function createPretender(server) {
-  if (typeof window !== "undefined") {
-    return new Pretender(
-      function () {
-        this.passthroughRequest = function (verb, path, request) {
-          if (server.shouldLog()) {
-            console.log(
-              `Mirage: Passthrough request for ${verb.toUpperCase()} ${
-                request.url
-              }`
-            );
-          }
-        };
-
-        this.handledRequest = function (verb, path, request) {
-          if (server.shouldLog()) {
-            console.groupCollapsed(
-              `Mirage: [${request.status}] ${verb.toUpperCase()} ${request.url}`
-            );
-            let { requestBody, responseText } = request;
-            let loggedRequest, loggedResponse;
-
-            try {
-              loggedRequest = JSON.parse(requestBody);
-            } catch (e) {
-              loggedRequest = requestBody;
-            }
-
-            try {
-              loggedResponse = JSON.parse(responseText);
-            } catch (e) {
-              loggedResponse = responseText;
-            }
-
-            console.groupCollapsed("Response");
-            console.log(loggedResponse);
-            console.groupEnd();
-
-            console.groupCollapsed("Request (data)");
-            console.log(loggedRequest);
-            console.groupEnd();
-
-            console.groupCollapsed("Request (raw)");
-            console.log(request);
-            console.groupEnd();
-
-            console.groupEnd();
-          }
-        };
+import PretenderConfig from "./mock-server/pretender-config";
 
-        let originalCheckPassthrough = this.checkPassthrough;
-        this.checkPassthrough = function (request) {
-          let shouldPassthrough = server.passthroughChecks.some(
-            (passthroughCheck) => passthroughCheck(request)
-          );
-
-          if (shouldPassthrough) {
-            let url = request.url.includes("?")
-              ? request.url.substr(0, request.url.indexOf("?"))
-              : request.url;
-
-            this[request.method.toLowerCase()](url, this.passthrough);
-          }
-
-          return originalCheckPassthrough.apply(this, arguments);
-        };
-
-        this.unhandledRequest = function (verb, path) {
-          path = decodeURI(path);
-          assert(
-            `Your app tried to ${verb} '${path}', but there was no route defined to handle this request. Define a route for this endpoint in your routes() config. Did you forget to define a namespace?`
-          );
-        };
-      },
-      { trackRequests: server.shouldTrackRequests() }
-    );
-  }
-}
+const isPluralForModelCache = {};
 
 const defaultRouteOptions = {
   coalesce: false,
@@ -111,22 +23,6 @@ const defaultRouteOptions = {
 
 const defaultInflector = { singularize, pluralize };
 
-/**
-  @hide
-*/
-const defaultPassthroughs = [
-  "http://localhost:0/chromecheckurl", // mobile chrome
-  "http://localhost:30820/socket.io", // electron
-  (request) => {
-    return /.+\.hot-update.json$/.test(request.url);
-  },
-];
-
-/**
-  @hide
-*/
-export { defaultPassthroughs };
-
 /**
  * Determine if the object contains a valid option.
  *
@@ -253,8 +149,49 @@ export default class Server {
     this.schema = this.schema || undefined;
   }
 
+  // todo deprecate following
+  get namespace() {
+    return this.interceptor.namespace;
+  }
+  set namespace(value) {
+    this.interceptor.namespace = value;
+  }
+
+  // todo deprecate following
+  get urlPrefix() {
+    return this.interceptor.urlPrefix;
+  }
+  set urlPrefix(value) {
+    this.interceptor.urlPrefix = value;
+  }
+
+  // todo deprecate following
+  get timing() {
+    return this.interceptor.timing;
+  }
+  set timing(value) {
+    this.interceptor.timing = value;
+  }
+
+  // todo deprecate following
+  get passthroughChecks() {
+    return this.interceptor.passthroughChecks;
+  }
+  set passthroughChecks(value) {
+    this.interceptor.passthroughChecks = value;
+  }
+
   config(config = {}) {
-    this.passthroughChecks = this.passthroughChecks || [];
+    if (!config.interceptor) {
+      config.interceptor = new PretenderConfig();
+    }
+
+    if (this.interceptor) {
+      this.interceptor.config(config);
+    } else {
+      this.interceptor = config.interceptor;
+      this.interceptor.create(this, config);
+    }
 
     let didOverrideConfig =
       config.environment &&
@@ -284,48 +221,6 @@ export default class Server {
 
     this._config = config;
 
-    /**
-      Set the base namespace used for all routes defined with `get`, `post`, `put` or `del`.
-
-      For example,
-
-      ```js
-      createServer({
-        routes() {
-          this.namespace = '/api';
-
-          // this route will handle the URL '/api/contacts'
-          this.get('/contacts', 'contacts');
-        }
-      })
-      ```
-
-      Note that only routes defined after `this.namespace` are affected. This is useful if you have a few one-off routes that you don't want under your namespace:
-
-      ```js
-      createServer({
-        routes() {
-
-          // this route handles /auth
-          this.get('/auth', function() { ...});
-
-          this.namespace = '/api';
-          // this route will handle the URL '/api/contacts'
-          this.get('/contacts', 'contacts');
-        };
-      })
-      ```
-
-      If your app is loaded from the filesystem vs. a server (e.g. via Cordova or Electron vs. `localhost` or `https://yourhost.com/`), you will need to explicitly define a namespace. Likely values are `/` (if requests are made with relative paths) or `https://yourhost.com/api/...` (if requests are made to a defined server).
-
-      For a sample implementation leveraging a configured API host & namespace, check out [this issue comment](https://github.com/miragejs/ember-cli-mirage/issues/497#issuecomment-183458721).
-
-      @property namespace
-      @type String
-      @public
-    */
-    this.namespace = this.namespace || config.namespace || "";
-
     /**
       Mirage needs know the singular and plural versions of certain words for some of its APIs to work.
 
@@ -371,42 +266,6 @@ export default class Server {
     this.inflector = config.inflector || defaultInflector;
     this._container.register("inflector", this.inflector);
 
-    /**
-      Sets a string to prefix all route handler URLs with.
-
-      Useful if your app makes API requests to a different port.
-
-      ```js
-      createServer({
-        routes() {
-          this.urlPrefix = 'http://localhost:8080'
-        }
-      })
-      ```
-    */
-    this.urlPrefix = this.urlPrefix || config.urlPrefix || "";
-
-    /**
-      Set the number of milliseconds for the the Server's response time.
-
-      By default there's a 400ms delay during development, and 0 delay in testing (so your tests run fast).
-
-      ```js
-      createServer({
-        routes() {
-          this.timing = 400; // default
-        }
-      })
-      ```
-
-      To set the timing for individual routes, see the `timing` option for route handlers.
-
-      @property timing
-      @type Number
-      @public
-    */
-    this.timing = this.timing || config.timing || 400;
-
     /**
       Set to `true` or `false` to explicitly specify logging behavior.
 
@@ -443,14 +302,12 @@ export default class Server {
       @return {Boolean}
       @public
     */
-    this.logging = this.logging !== undefined ? this.logging : undefined;
+    this.logging = config.logging !== undefined ? this.logging : undefined;
 
     this.testConfig = this.testConfig || undefined;
 
     this.trackRequests = config.trackRequests;
 
-    this._defineRouteHandlerHelpers();
-
     if (this.db) {
       this.db.registerIdentityManagers(config.identityManagers);
     } else {
@@ -478,35 +335,6 @@ export default class Server {
       config.scenarios &&
       Object.prototype.hasOwnProperty.call(config.scenarios, "default");
 
-    let didOverridePretenderConfig =
-      config.trackRequests !== undefined && this.pretender;
-    assert(
-      !didOverridePretenderConfig,
-      "You cannot modify Pretender's request tracking once the server is created"
-    );
-
-    /**
-      Mirage uses [pretender.js](https://github.com/trek/pretender) as its xhttp interceptor. In your Mirage config, `this.pretender` refers to the actual Pretender instance, so any config options that work there will work here as well.
-
-      ```js
-      createServer({
-        routes() {
-          this.pretender.handledRequest = (verb, path, request) => {
-            console.log(`Your server responded to ${path}`);
-          }
-        }
-      })
-      ```
-
-      Refer to [Pretender's docs](https://github.com/pretenderjs/pretender) if you want to change any options on your Pretender instance.
-
-      @property pretender
-      @return {Object} The Pretender instance
-      @public
-    */
-    this.pretender =
-      this.pretender || config.pretender || createPretender(this);
-
     if (config.baseConfig) {
       this.loadConfig(config.baseConfig);
     }
@@ -530,13 +358,7 @@ export default class Server {
       this.loadFixtures();
     }
 
-    let useDefaultPassthroughs =
-      typeof config.useDefaultPassthroughs !== "undefined"
-        ? config.useDefaultPassthroughs
-        : true;
-    if (useDefaultPassthroughs) {
-      this._configureDefaultPassthroughs();
-    }
+    this.interceptor.start?.();
   }
 
   /**
@@ -590,86 +412,9 @@ export default class Server {
     this.timing = this.isTest() ? 0 : this.timing || 0;
   }
 
-  /**
-    By default, if your app makes a request that is not defined in your server config, Mirage will throw an error. You can use `passthrough` to whitelist requests, and allow them to pass through your Mirage server to the actual network layer.
-
-    Note: Put all passthrough config at the bottom of your routes, to give your route handlers precedence.
-
-    To ignore paths on your current host (as well as configured `namespace`), use a leading `/`:
-
-    ```js
-    this.passthrough('/addresses');
-    ```
-
-    You can also pass a list of paths, or call `passthrough` multiple times:
-
-    ```js
-    this.passthrough('/addresses', '/contacts');
-    this.passthrough('/something');
-    this.passthrough('/else');
-    ```
-
-    These lines will allow all HTTP verbs to pass through. If you want only certain verbs to pass through, pass an array as the last argument with the specified verbs:
-
-    ```js
-    this.passthrough('/addresses', ['post']);
-    this.passthrough('/contacts', '/photos', ['get']);
-    ```
-
-    You can pass a function to `passthrough` to do a runtime check on whether or not the request should be handled by Mirage. If the function returns `true` Mirage will not handle the request and let it pass through.
-
-    ```js
-    this.passthrough(request => {
-      return request.queryParams.skipMirage;
-    });
-    ```
-
-    If you want all requests on the current domain to pass through, simply invoke the method with no arguments:
-
-    ```js
-    this.passthrough();
-    ```
-
-    Note again that the current namespace (i.e. any `namespace` property defined above this call) will be applied.
-
-    You can also allow other-origin hosts to passthrough. If you use a fully-qualified domain name, the `namespace` property will be ignored. Use two * wildcards to match all requests under a path:
-
-    ```js
-    this.passthrough('http://api.foo.bar/**');
-    this.passthrough('http://api.twitter.com/v1/cards/**');
-    ```
-
-    In versions of Pretender prior to 0.12, `passthrough` only worked with jQuery >= 2.x. As long as you're on Pretender@0.12 or higher, you should be all set.
-
-    @method passthrough
-    @param {String} [...paths] Any number of paths to whitelist
-    @param {Array} options Unused
-    @public
-  */
+  // TODO deprecate this in favor of direct call
   passthrough(...paths) {
-    // this only works in browser-like environments for now. in node users will have to configure
-    // their own interceptor if they are using one.
-    if (typeof window !== "undefined") {
-      let verbs = ["get", "post", "put", "delete", "patch", "options", "head"];
-      let lastArg = paths[paths.length - 1];
-
-      if (paths.length === 0) {
-        paths = ["/**", "/"];
-      } else if (Array.isArray(lastArg)) {
-        verbs = paths.pop();
-      }
-
-      paths.forEach((path) => {
-        if (typeof path === "function") {
-          this.passthroughChecks.push(path);
-        } else {
-          verbs.forEach((verb) => {
-            let fullPath = this._getFullPath(path);
-            this.pretender[verb](fullPath, this.pretender.passthrough);
-          });
-        }
-      });
-    }
+    this.interceptor.passthrough?.(...paths);
   }
 
   /**
@@ -983,7 +728,7 @@ export default class Server {
   */
   shutdown() {
     if (typeof window !== "undefined") {
-      this.pretender.shutdown();
+      this.interceptor.shutdown();
     }
 
     if (typeof window !== "undefined" && this.environment === "test") {
@@ -1027,38 +772,6 @@ export default class Server {
     });
   }
 
-  /**
-   *
-   * @private
-   * @hide
-   */
-  _defineRouteHandlerHelpers() {
-    [
-      ["get"],
-      ["post"],
-      ["put"],
-      ["delete", "del"],
-      ["patch"],
-      ["head"],
-      ["options"],
-    ].forEach(([verb, alias]) => {
-      this[verb] = (path, ...args) => {
-        let [rawHandler, customizedCode, options] = extractRouteArguments(args);
-        return this._registerRouteHandler(
-          verb,
-          path,
-          rawHandler,
-          customizedCode,
-          options
-        );
-      };
-
-      if (alias) {
-        this[alias] = this[verb];
-      }
-    });
-  }
-
   _serialize(body) {
     if (typeof body === "string") {
       return body;
@@ -1067,6 +780,17 @@ export default class Server {
     }
   }
 
+  registerRouteHandler(verb, path, args) {
+    let [rawHandler, customizedCode, options] = extractRouteArguments(args);
+    return this._registerRouteHandler(
+      verb,
+      path,
+      rawHandler,
+      customizedCode,
+      options
+    );
+  }
+
   _registerRouteHandler(verb, path, rawHandler, customizedCode, options) {
     let routeHandler = this._container.create("RouteHandler", {
       schema: this.schema,
@@ -1078,23 +802,13 @@ export default class Server {
       serializerOrRegistry: this.serializerOrRegistry,
     });
 
-    let fullPath = this._getFullPath(path);
-    let timing =
-      options.timing !== undefined ? options.timing : () => this.timing;
-
-    if (this.pretender) {
-      return this.pretender[verb](
-        fullPath,
-        (request) => {
-          return routeHandler.handle(request).then((mirageResponse) => {
-            let [code, headers, response] = mirageResponse;
-
-            return [code, headers, this._serialize(response)];
-          });
-        },
-        timing
-      );
-    }
+    return (request) => {
+      return routeHandler.handle(request).then((mirageResponse) => {
+        let [code, headers, response] = mirageResponse;
+
+        return [code, headers, this._serialize(response)];
+      });
+    };
   }
 
   /**
@@ -1107,135 +821,6 @@ export default class Server {
     return modulesOfType ? Object.keys(modulesOfType).length > 0 : false;
   }
 
-  /**
-   * Builds a full path for Pretender to monitor based on the `path` and
-   * configured options (`urlPrefix` and `namespace`).
-   *
-   * @private
-   * @hide
-   */
-  _getFullPath(path) {
-    path = path[0] === "/" ? path.slice(1) : path;
-    let fullPath = "";
-    let urlPrefix = this.urlPrefix ? this.urlPrefix.trim() : "";
-    let namespace = "";
-
-    // if there is a urlPrefix and a namespace
-    if (this.urlPrefix && this.namespace) {
-      if (
-        this.namespace[0] === "/" &&
-        this.namespace[this.namespace.length - 1] === "/"
-      ) {
-        namespace = this.namespace
-          .substring(0, this.namespace.length - 1)
-          .substring(1);
-      }
-
-      if (
-        this.namespace[0] === "/" &&
-        this.namespace[this.namespace.length - 1] !== "/"
-      ) {
-        namespace = this.namespace.substring(1);
-      }
-
-      if (
-        this.namespace[0] !== "/" &&
-        this.namespace[this.namespace.length - 1] === "/"
-      ) {
-        namespace = this.namespace.substring(0, this.namespace.length - 1);
-      }
-
-      if (
-        this.namespace[0] !== "/" &&
-        this.namespace[this.namespace.length - 1] !== "/"
-      ) {
-        namespace = this.namespace;
-      }
-    }
-
-    // if there is a namespace and no urlPrefix
-    if (this.namespace && !this.urlPrefix) {
-      if (
-        this.namespace[0] === "/" &&
-        this.namespace[this.namespace.length - 1] === "/"
-      ) {
-        namespace = this.namespace.substring(0, this.namespace.length - 1);
-      }
-
-      if (
-        this.namespace[0] === "/" &&
-        this.namespace[this.namespace.length - 1] !== "/"
-      ) {
-        namespace = this.namespace;
-      }
-
-      if (
-        this.namespace[0] !== "/" &&
-        this.namespace[this.namespace.length - 1] === "/"
-      ) {
-        let namespaceSub = this.namespace.substring(
-          0,
-          this.namespace.length - 1
-        );
-        namespace = `/${namespaceSub}`;
-      }
-
-      if (
-        this.namespace[0] !== "/" &&
-        this.namespace[this.namespace.length - 1] !== "/"
-      ) {
-        namespace = `/${this.namespace}`;
-      }
-    }
-
-    // if no namespace
-    if (!this.namespace) {
-      namespace = "";
-    }
-
-    // check to see if path is a FQDN. if so, ignore any urlPrefix/namespace that was set
-    if (/^https?:\/\//.test(path)) {
-      fullPath += path;
-    } else {
-      // otherwise, if there is a urlPrefix, use that as the beginning of the path
-      if (urlPrefix.length) {
-        fullPath +=
-          urlPrefix[urlPrefix.length - 1] === "/" ? urlPrefix : `${urlPrefix}/`;
-      }
-
-      // add the namespace to the path
-      fullPath += namespace;
-
-      // add a trailing slash to the path if it doesn't already contain one
-      if (fullPath[fullPath.length - 1] !== "/") {
-        fullPath += "/";
-      }
-
-      // finally add the configured path
-      fullPath += path;
-
-      // if we're making a same-origin request, ensure a / is prepended and
-      // dedup any double slashes
-      if (!/^https?:\/\//.test(fullPath)) {
-        fullPath = `/${fullPath}`;
-        fullPath = fullPath.replace(/\/+/g, "/");
-      }
-    }
-
-    return fullPath;
-  }
-
-  /**
-   *
-   * @private
-   * @hide
-   */
-  _configureDefaultPassthroughs() {
-    defaultPassthroughs.forEach((passthroughUrl) => {
-      this.passthrough(passthroughUrl);
-    });
-  }
-
   /**
    *
    * @private


Test patch:
diff --git a/__tests__/external/shared/orm/belongs-to/1-basic/association-set-test.js b/__tests__/external/shared/orm/belongs-to/1-basic/association-set-test.js
index 111f715..b42d12b 100644
--- a/__tests__/external/shared/orm/belongs-to/1-basic/association-set-test.js
+++ b/__tests__/external/shared/orm/belongs-to/1-basic/association-set-test.js
@@ -1,6 +1,6 @@
 import Helper, { states } from "./_helper";
 
-describe("External | Shared | ORM | Belongs To | Basic | association #set", () => {
+describe("External | Shared | ORM | Belongs To | Basic | association #set", function () {
   let helper;
   beforeEach(() => {
     helper = new Helper();
diff --git a/__tests__/internal/move-after-handle-request/get-full-path-test.js b/__tests__/internal/move-after-handle-request/get-full-path-test.js
index 23caa0c..287934e 100644
--- a/__tests__/internal/move-after-handle-request/get-full-path-test.js
+++ b/__tests__/internal/move-after-handle-request/get-full-path-test.js
@@ -17,35 +17,41 @@ describe("Integration | Server | Get full path", function () {
     expect.assertions(1);
     server.namespace = "/api";
 
-    expect(server._getFullPath("/contacts")).toEqual("/api/contacts");
+    expect(server.interceptor._getFullPath("/contacts")).toEqual(
+      "/api/contacts"
+    );
   });
 
   test("it works with a configured namespace with a trailing slash", () => {
     expect.assertions(1);
     server.namespace = "api/";
 
-    expect(server._getFullPath("/contacts")).toEqual("/api/contacts");
+    expect(server.interceptor._getFullPath("/contacts")).toEqual(
+      "/api/contacts"
+    );
   });
 
   test("it works with a configured namespace without a leading slash", () => {
     expect.assertions(1);
     server.namespace = "api";
 
-    expect(server._getFullPath("/contacts")).toEqual("/api/contacts");
+    expect(server.interceptor._getFullPath("/contacts")).toEqual(
+      "/api/contacts"
+    );
   });
 
   test("it works with a configured namespace is an empty string", () => {
     expect.assertions(1);
     server.namespace = "";
 
-    expect(server._getFullPath("/contacts")).toEqual("/contacts");
+    expect(server.interceptor._getFullPath("/contacts")).toEqual("/contacts");
   });
 
   test("it works with a configured urlPrefix with a trailing slash", () => {
     expect.assertions(1);
     server.urlPrefix = "http://localhost:3000/";
 
-    expect(server._getFullPath("/contacts")).toEqual(
+    expect(server.interceptor._getFullPath("/contacts")).toEqual(
       "http://localhost:3000/contacts"
     );
   });
@@ -54,7 +60,7 @@ describe("Integration | Server | Get full path", function () {
     expect.assertions(1);
     server.urlPrefix = "http://localhost:3000";
 
-    expect(server._getFullPath("/contacts")).toEqual(
+    expect(server.interceptor._getFullPath("/contacts")).toEqual(
       "http://localhost:3000/contacts"
     );
   });
@@ -63,7 +69,7 @@ describe("Integration | Server | Get full path", function () {
     expect.assertions(1);
     server.urlPrefix = "";
 
-    expect(server._getFullPath("/contacts")).toEqual("/contacts");
+    expect(server.interceptor._getFullPath("/contacts")).toEqual("/contacts");
   });
 
   test("it works with a configured namespace and a urlPrefix", () => {
@@ -71,7 +77,7 @@ describe("Integration | Server | Get full path", function () {
     server.namespace = "api";
     server.urlPrefix = "http://localhost:3000";
 
-    expect(server._getFullPath("/contacts")).toEqual(
+    expect(server.interceptor._getFullPath("/contacts")).toEqual(
       "http://localhost:3000/api/contacts"
     );
   });
@@ -81,7 +87,7 @@ describe("Integration | Server | Get full path", function () {
     server.namespace = "/api";
     server.urlPrefix = "http://localhost:3000";
 
-    expect(server._getFullPath("/contacts")).toEqual(
+    expect(server.interceptor._getFullPath("/contacts")).toEqual(
       "http://localhost:3000/api/contacts"
     );
   });
@@ -91,6 +97,6 @@ describe("Integration | Server | Get full path", function () {
     server.namespace = "";
     server.urlPrefix = "";
 
-    expect(server._getFullPath("/contacts")).toEqual("/contacts");
+    expect(server.interceptor._getFullPath("/contacts")).toEqual("/contacts");
   });
 });