@schmock/core 2.4.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -0
- package/dist/abort.d.ts +11 -1
- package/dist/abort.js +13 -2
- package/dist/adapter.d.ts +21 -0
- package/dist/adapter.js +19 -0
- package/dist/admission.d.ts +21 -0
- package/dist/admission.js +39 -0
- package/dist/binary.d.ts +0 -1
- package/dist/builder.d.ts +16 -32
- package/dist/builder.js +425 -904
- package/dist/constants.d.ts +33 -2
- package/dist/constants.js +73 -1
- package/dist/debug-logger.d.ts +10 -0
- package/dist/debug-logger.js +31 -0
- package/dist/delay.d.ts +12 -0
- package/dist/delay.js +37 -0
- package/dist/errors.d.ts +13 -2
- package/dist/errors.js +21 -2
- package/dist/events.d.ts +17 -0
- package/dist/events.js +58 -0
- package/dist/generations.d.ts +48 -0
- package/dist/generations.js +82 -0
- package/dist/headers.d.ts +27 -0
- package/dist/headers.js +57 -0
- package/dist/helpers.d.ts +9 -10
- package/dist/helpers.js +4 -1
- package/dist/history.d.ts +56 -0
- package/dist/history.js +151 -0
- package/dist/http-helpers.d.ts +110 -5
- package/dist/http-helpers.js +328 -46
- package/dist/index.d.ts +295 -31
- package/dist/index.js +17 -9
- package/dist/interceptor.d.ts +40 -10
- package/dist/interceptor.js +412 -178
- package/dist/node-server.d.ts +27 -0
- package/dist/node-server.js +166 -0
- package/dist/parser.d.ts +0 -1
- package/dist/parser.js +145 -22
- package/dist/plugin-hooks.d.ts +57 -0
- package/dist/plugin-hooks.js +276 -0
- package/dist/plugin-pipeline.d.ts +0 -1
- package/dist/plugin-pipeline.js +25 -4
- package/dist/response-normalizer.d.ts +36 -1
- package/dist/response-normalizer.js +102 -0
- package/dist/response-parser.d.ts +19 -1
- package/dist/response-parser.js +77 -19
- package/dist/route-matcher.d.ts +0 -1
- package/dist/route-table.d.ts +64 -0
- package/dist/route-table.js +220 -0
- package/dist/snapshot.d.ts +14 -0
- package/dist/snapshot.js +98 -0
- package/dist/types.d.ts +34 -1
- package/package.json +8 -3
- package/dist/abort.d.ts.map +0 -1
- package/dist/binary.d.ts.map +0 -1
- package/dist/builder.d.ts.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/helpers.d.ts.map +0 -1
- package/dist/http-helpers.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/interceptor.d.ts.map +0 -1
- package/dist/parser.d.ts.map +0 -1
- package/dist/plugin-pipeline.d.ts.map +0 -1
- package/dist/response-normalizer.d.ts.map +0 -1
- package/dist/response-parser.d.ts.map +0 -1
- package/dist/route-matcher.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DebugLogger } from "./debug-logger.js";
|
|
2
|
+
interface NodeServerControllerOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Admit one request against the mock. Called on arrival, before the request
|
|
5
|
+
* is parsed, and released once it is answered.
|
|
6
|
+
*/
|
|
7
|
+
readonly admitRequest: () => Schmock.RequestAdmission;
|
|
8
|
+
readonly logger: DebugLogger;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The standalone HTTP server behind `mock.listen()` / `mock.close()`.
|
|
12
|
+
*
|
|
13
|
+
* Owns the start and close state machines: at most one server is running or
|
|
14
|
+
* starting, a `close()` during start-up cancels the start, and a new start
|
|
15
|
+
* waits for every earlier server to finish closing (the close barrier) so a
|
|
16
|
+
* restart on the same port never races the old socket.
|
|
17
|
+
*
|
|
18
|
+
* `node:http` is imported lazily, on the first `listen()`, so a browser bundle
|
|
19
|
+
* that never listens never pulls it in (issue #395).
|
|
20
|
+
*/
|
|
21
|
+
export declare class NodeServerController {
|
|
22
|
+
#private;
|
|
23
|
+
constructor(options: NodeServerControllerOptions);
|
|
24
|
+
listen(port: number, hostname: string): Promise<Schmock.ServerInfo>;
|
|
25
|
+
close(): void;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { errorMessage, SchmockError } from "./errors.js";
|
|
2
|
+
import { DEFAULT_MAX_BODY_SIZE, serveNodeRequest } from "./http-helpers.js";
|
|
3
|
+
/**
|
|
4
|
+
* The standalone HTTP server behind `mock.listen()` / `mock.close()`.
|
|
5
|
+
*
|
|
6
|
+
* Owns the start and close state machines: at most one server is running or
|
|
7
|
+
* starting, a `close()` during start-up cancels the start, and a new start
|
|
8
|
+
* waits for every earlier server to finish closing (the close barrier) so a
|
|
9
|
+
* restart on the same port never races the old socket.
|
|
10
|
+
*
|
|
11
|
+
* `node:http` is imported lazily, on the first `listen()`, so a browser bundle
|
|
12
|
+
* that never listens never pulls it in (issue #395).
|
|
13
|
+
*/
|
|
14
|
+
export class NodeServerController {
|
|
15
|
+
#server;
|
|
16
|
+
#pendingStart;
|
|
17
|
+
#closeBarrier;
|
|
18
|
+
#admitRequest;
|
|
19
|
+
#logger;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.#admitRequest = options.admitRequest;
|
|
22
|
+
this.#logger = options.logger;
|
|
23
|
+
}
|
|
24
|
+
listen(port, hostname) {
|
|
25
|
+
if (this.#server || this.#pendingStart) {
|
|
26
|
+
throw new SchmockError("Server is already running", "SERVER_ALREADY_RUNNING");
|
|
27
|
+
}
|
|
28
|
+
let resolveStart = (_info) => { };
|
|
29
|
+
let rejectStart = (_error) => { };
|
|
30
|
+
const startPromise = new Promise((resolve, reject) => {
|
|
31
|
+
resolveStart = resolve;
|
|
32
|
+
rejectStart = reject;
|
|
33
|
+
});
|
|
34
|
+
const operation = {
|
|
35
|
+
token: Symbol("schmock.server.start"),
|
|
36
|
+
port,
|
|
37
|
+
hostname,
|
|
38
|
+
resolve: resolveStart,
|
|
39
|
+
reject: rejectStart,
|
|
40
|
+
settled: false,
|
|
41
|
+
};
|
|
42
|
+
this.#pendingStart = operation;
|
|
43
|
+
const closeBarrier = this.#closeBarrier ?? Promise.resolve();
|
|
44
|
+
void closeBarrier
|
|
45
|
+
// Lazy-load node:http so browser bundles never pull it in (issue #395).
|
|
46
|
+
// The rejection handler must sit on the import() expression itself:
|
|
47
|
+
// esbuild (and so the Angular application builder) leaves a dynamic
|
|
48
|
+
// import unresolved only when that expression handles its own failure,
|
|
49
|
+
// and the outer .catch() below does not count. Without it a
|
|
50
|
+
// `platform: "browser"` build fails with `Could not resolve "node:http"`.
|
|
51
|
+
.then(() => import("node:http").catch((error) => {
|
|
52
|
+
throw error;
|
|
53
|
+
}))
|
|
54
|
+
.then(({ createServer }) => {
|
|
55
|
+
if (!this.#ownsServerStart(operation))
|
|
56
|
+
return;
|
|
57
|
+
this.#startHttpServer(operation, createServer);
|
|
58
|
+
})
|
|
59
|
+
.catch((error) => {
|
|
60
|
+
this.#rejectServerStart(operation, error);
|
|
61
|
+
});
|
|
62
|
+
return startPromise;
|
|
63
|
+
}
|
|
64
|
+
close() {
|
|
65
|
+
this.#cancelServerStart();
|
|
66
|
+
const server = this.#server;
|
|
67
|
+
if (!server)
|
|
68
|
+
return;
|
|
69
|
+
this.#server = undefined;
|
|
70
|
+
this.#beginServerClose(server);
|
|
71
|
+
this.#logger.log("server", "Server stopped");
|
|
72
|
+
}
|
|
73
|
+
#ownsServerStart(operation) {
|
|
74
|
+
return this.#pendingStart === operation && !operation.settled;
|
|
75
|
+
}
|
|
76
|
+
#startHttpServer(operation, createServer) {
|
|
77
|
+
const httpServer = createServer((req, res) => {
|
|
78
|
+
// Admitted on arrival, before the request is parsed, so a reset() while
|
|
79
|
+
// its body uploads neither changes its routes nor uninstalls its plugins.
|
|
80
|
+
const admittedRequest = this.#admitRequest();
|
|
81
|
+
void serveNodeRequest(req, res, {
|
|
82
|
+
handle: admittedRequest.handle,
|
|
83
|
+
maxBodySize: DEFAULT_MAX_BODY_SIZE,
|
|
84
|
+
}).finally(() => admittedRequest.release());
|
|
85
|
+
});
|
|
86
|
+
operation.server = httpServer;
|
|
87
|
+
const handleStartupError = (error) => {
|
|
88
|
+
this.#rejectServerStart(operation, error);
|
|
89
|
+
};
|
|
90
|
+
httpServer.once("error", handleStartupError);
|
|
91
|
+
// Once listening, a server-level 'error' (an accept failure such as
|
|
92
|
+
// EMFILE) must still have a listener: with none, Node rethrows it as an
|
|
93
|
+
// uncaught exception and takes the whole test runner down.
|
|
94
|
+
const reportServerError = (error) => {
|
|
95
|
+
this.#logger.log("server", `Server error: ${errorMessage(error)}`);
|
|
96
|
+
};
|
|
97
|
+
try {
|
|
98
|
+
httpServer.listen(operation.port, operation.hostname, () => {
|
|
99
|
+
// Attach the permanent reporter BEFORE dropping the startup handler:
|
|
100
|
+
// the other order leaves a window with no 'error' listener at all.
|
|
101
|
+
httpServer.on("error", reportServerError);
|
|
102
|
+
httpServer.off("error", handleStartupError);
|
|
103
|
+
if (!this.#ownsServerStart(operation)) {
|
|
104
|
+
this.#beginServerClose(httpServer);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const addr = httpServer.address();
|
|
108
|
+
const actualPort = addr !== null && typeof addr === "object"
|
|
109
|
+
? addr.port
|
|
110
|
+
: operation.port;
|
|
111
|
+
const info = { port: actualPort, hostname: operation.hostname };
|
|
112
|
+
operation.settled = true;
|
|
113
|
+
this.#pendingStart = undefined;
|
|
114
|
+
this.#server = httpServer;
|
|
115
|
+
this.#logger.log("server", `Listening on ${operation.hostname}:${actualPort}`);
|
|
116
|
+
operation.resolve(info);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
httpServer.off("error", handleStartupError);
|
|
121
|
+
this.#rejectServerStart(operation, error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
#rejectServerStart(operation, error) {
|
|
125
|
+
if (operation.settled)
|
|
126
|
+
return;
|
|
127
|
+
operation.settled = true;
|
|
128
|
+
if (this.#pendingStart === operation) {
|
|
129
|
+
this.#pendingStart = undefined;
|
|
130
|
+
}
|
|
131
|
+
if (operation.server) {
|
|
132
|
+
this.#beginServerClose(operation.server);
|
|
133
|
+
}
|
|
134
|
+
operation.reject(error);
|
|
135
|
+
}
|
|
136
|
+
#cancelServerStart() {
|
|
137
|
+
const operation = this.#pendingStart;
|
|
138
|
+
if (!operation)
|
|
139
|
+
return;
|
|
140
|
+
this.#rejectServerStart(operation, new SchmockError("Server start was cancelled", "SERVER_START_CANCELLED"));
|
|
141
|
+
}
|
|
142
|
+
#beginServerClose(server) {
|
|
143
|
+
const closePromise = new Promise((resolve) => {
|
|
144
|
+
try {
|
|
145
|
+
server.close(() => resolve());
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
resolve();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
try {
|
|
152
|
+
server.closeAllConnections();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// A not-yet-listening server has no connections to close.
|
|
156
|
+
}
|
|
157
|
+
const previousBarrier = this.#closeBarrier ?? Promise.resolve();
|
|
158
|
+
const combinedBarrier = Promise.all([previousBarrier, closePromise]).then(() => undefined);
|
|
159
|
+
this.#closeBarrier = combinedBarrier;
|
|
160
|
+
void combinedBarrier.finally(() => {
|
|
161
|
+
if (this.#closeBarrier === combinedBarrier) {
|
|
162
|
+
this.#closeBarrier = undefined;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
package/dist/parser.d.ts
CHANGED
package/dist/parser.js
CHANGED
|
@@ -22,32 +22,155 @@ export function parseRouteKey(routeKey) {
|
|
|
22
22
|
throw new RouteParseError(routeKey, 'Expected format: "METHOD /path" (e.g., "GET /users")');
|
|
23
23
|
}
|
|
24
24
|
const [, method, rawPath] = match;
|
|
25
|
-
//
|
|
25
|
+
// Tokenize once, then derive `path`, `pattern` and `params` from the same
|
|
26
|
+
// tokens. `path` is canonicalized here so the duplicate check, the
|
|
26
27
|
// static-route Map key and getRoutes() all agree on one spelling: the
|
|
27
28
|
// percent-encoded transport form with a single trailing slash stripped.
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
// literals (".json", brackets, parens, etc.) terminate the name and can
|
|
31
|
-
// be escaped without bleeding into the parameter regex. Build the
|
|
32
|
-
// pattern by splitting the path on the param marker, escaping each
|
|
33
|
-
// literal segment, then substituting the capture group for each :name.
|
|
34
|
-
const params = [];
|
|
35
|
-
const regexPath = path
|
|
36
|
-
.split(/(:[a-zA-Z0-9_-]+)/g)
|
|
37
|
-
.map((segment) => {
|
|
38
|
-
const paramMatch = segment.match(/^:([a-zA-Z0-9_-]+)$/);
|
|
39
|
-
if (paramMatch) {
|
|
40
|
-
params.push(paramMatch[1]);
|
|
41
|
-
return "([^/]+)";
|
|
42
|
-
}
|
|
43
|
-
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
44
|
-
})
|
|
45
|
-
.join("");
|
|
46
|
-
const pattern = new RegExp(`^${regexPath}$`);
|
|
29
|
+
const tokens = tokenizeRoutePath(routeKey, normalizePath(rawPath));
|
|
30
|
+
const params = tokens.flatMap((token) => token.kind === "param" ? [token.name] : []);
|
|
47
31
|
return {
|
|
48
32
|
method: toHttpMethod(method),
|
|
49
|
-
path,
|
|
50
|
-
pattern,
|
|
33
|
+
path: serializeRoutePath(tokens, params.length > 0),
|
|
34
|
+
pattern: new RegExp(`^${compileRoutePattern(tokens)}$`),
|
|
51
35
|
params,
|
|
52
36
|
};
|
|
53
37
|
}
|
|
38
|
+
const PLAIN_NAME_CHAR = /^[A-Za-z0-9_-]$/;
|
|
39
|
+
const TRAILING_HYPHENS = /-+$/;
|
|
40
|
+
/** A bare colon that a later parse would read as the start of a parameter. */
|
|
41
|
+
const COLON_BEFORE_NAME = /:(?=[A-Za-z0-9_-])/g;
|
|
42
|
+
/**
|
|
43
|
+
* Read the parameter whose ':' sits at `index`, or undefined when that colon
|
|
44
|
+
* is a literal: nothing name-like follows it, or the quote is unclosed or
|
|
45
|
+
* empty (both of which were literal text before quoted names existed).
|
|
46
|
+
*/
|
|
47
|
+
function readParam(path, index) {
|
|
48
|
+
if (path[index] !== ":")
|
|
49
|
+
return undefined;
|
|
50
|
+
if (path[index + 1] === '"') {
|
|
51
|
+
const close = path.indexOf('"', index + 2);
|
|
52
|
+
if (close <= index + 2)
|
|
53
|
+
return undefined;
|
|
54
|
+
return { name: path.slice(index + 2, close), quoted: true, end: close + 1 };
|
|
55
|
+
}
|
|
56
|
+
let end = index + 1;
|
|
57
|
+
while (end < path.length && PLAIN_NAME_CHAR.test(path[end]))
|
|
58
|
+
end += 1;
|
|
59
|
+
if (end === index + 1)
|
|
60
|
+
return undefined;
|
|
61
|
+
return { name: path.slice(index + 1, end), quoted: false, end };
|
|
62
|
+
}
|
|
63
|
+
function tokenizeRoutePath(routeKey, path) {
|
|
64
|
+
const tokens = [];
|
|
65
|
+
let literal = "";
|
|
66
|
+
const pushParam = (name, quoted) => {
|
|
67
|
+
if (literal) {
|
|
68
|
+
tokens.push({ kind: "literal", text: literal });
|
|
69
|
+
literal = "";
|
|
70
|
+
}
|
|
71
|
+
const previous = tokens[tokens.length - 1];
|
|
72
|
+
if (previous?.kind === "param") {
|
|
73
|
+
throw new RouteParseError(routeKey, `Parameters ":${previous.name}" and ":${name}" are adjacent, so nothing decides where one ends. Put a literal between them (e.g. ":${previous.name}-:${name}"), or write a literal colon as "\\:".`);
|
|
74
|
+
}
|
|
75
|
+
tokens.push({ kind: "param", name, quoted });
|
|
76
|
+
};
|
|
77
|
+
let index = 0;
|
|
78
|
+
while (index < path.length) {
|
|
79
|
+
if (path[index] === "\\" && path[index + 1] === ":") {
|
|
80
|
+
literal += ":";
|
|
81
|
+
index += 2;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const param = readParam(path, index);
|
|
85
|
+
if (!param) {
|
|
86
|
+
literal += path[index];
|
|
87
|
+
index += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!param.quoted && readParam(path, param.end)) {
|
|
91
|
+
const name = param.name.replace(TRAILING_HYPHENS, "");
|
|
92
|
+
if (name) {
|
|
93
|
+
pushParam(name, false);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// A name made only of hyphens leaves nothing to name: the colon is
|
|
97
|
+
// literal text, as it would be without the parameter that follows.
|
|
98
|
+
literal += ":";
|
|
99
|
+
}
|
|
100
|
+
literal += param.name.slice(name.length);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
pushParam(param.name, param.quoted);
|
|
104
|
+
}
|
|
105
|
+
index = param.end;
|
|
106
|
+
}
|
|
107
|
+
if (literal)
|
|
108
|
+
tokens.push({ kind: "literal", text: literal });
|
|
109
|
+
return tokens;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The route's canonical spelling: literals in the percent-encoded transport
|
|
113
|
+
* form (see canonicalizePath) and parameters as written. A route with
|
|
114
|
+
* parameters keeps a `\:` escape wherever a bare colon would start a
|
|
115
|
+
* parameter. A route without parameters is spelled as its literal path,
|
|
116
|
+
* because that string is also the static-route key a request is looked up by.
|
|
117
|
+
*/
|
|
118
|
+
function serializeRoutePath(tokens, hasParams) {
|
|
119
|
+
return tokens
|
|
120
|
+
.map((token) => {
|
|
121
|
+
if (token.kind === "param") {
|
|
122
|
+
return token.quoted ? `:"${token.name}"` : `:${token.name}`;
|
|
123
|
+
}
|
|
124
|
+
const canonical = canonicalizePath(token.text);
|
|
125
|
+
return hasParams
|
|
126
|
+
? canonical.replace(COLON_BEFORE_NAME, "\\:")
|
|
127
|
+
: canonical;
|
|
128
|
+
})
|
|
129
|
+
.join("");
|
|
130
|
+
}
|
|
131
|
+
function escapeRegExp(text) {
|
|
132
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
133
|
+
}
|
|
134
|
+
function escapeCharClass(char) {
|
|
135
|
+
return char.replace(/[\\\]^-]/g, "\\$&");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Compile tokens to a regex source.
|
|
139
|
+
*
|
|
140
|
+
* A parameter that is the only one in its path segment, or the last one,
|
|
141
|
+
* captures `[^/]+` exactly as it always has — so `:name.json` still matches
|
|
142
|
+
* "report.v2.json" greedily. A parameter followed in the same segment by a
|
|
143
|
+
* literal and then another parameter also excludes that literal's first
|
|
144
|
+
* character, so it can only end at the separator:
|
|
145
|
+
* `:year-:month-:day` → `([^/-]+)-([^/-]+)-([^/]+)`. A percent-encoded
|
|
146
|
+
* literal is excluded whole rather than by its "%": `:first :last` →
|
|
147
|
+
* `((?:(?!%20)[^/])+)%20([^/]+)`. Without the exclusion
|
|
148
|
+
* every capture could end anywhere, and a long non-matching segment
|
|
149
|
+
* backtracked quadratically (two parameters) or cubically (three).
|
|
150
|
+
*/
|
|
151
|
+
function compileRoutePattern(tokens) {
|
|
152
|
+
return tokens
|
|
153
|
+
.map((token, index) => {
|
|
154
|
+
if (token.kind === "literal") {
|
|
155
|
+
return escapeRegExp(canonicalizePath(token.text));
|
|
156
|
+
}
|
|
157
|
+
const next = tokens[index + 1];
|
|
158
|
+
if (next?.kind === "literal" &&
|
|
159
|
+
!next.text.includes("/") &&
|
|
160
|
+
tokens[index + 2]?.kind === "param") {
|
|
161
|
+
const separator = canonicalizePath(next.text);
|
|
162
|
+
// A separator that canonicalizes to a percent triplet (a space, a
|
|
163
|
+
// brace, any non-ASCII character) starts with "%", and excluding "%"
|
|
164
|
+
// itself would forbid every encoded character in the capture ("José"
|
|
165
|
+
// is "Jos%C3%A9"). Exclude the whole encoded literal instead: the
|
|
166
|
+
// capture still cannot contain the separator, so it can only end at
|
|
167
|
+
// its first occurrence and the match stays linear.
|
|
168
|
+
if (separator.startsWith("%")) {
|
|
169
|
+
return `((?:(?!${escapeRegExp(separator)})[^/])+)`;
|
|
170
|
+
}
|
|
171
|
+
return `([^/${escapeCharClass(separator[0])}]+)`;
|
|
172
|
+
}
|
|
173
|
+
return "([^/]+)";
|
|
174
|
+
})
|
|
175
|
+
.join("");
|
|
176
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { DebugLogger } from "./debug-logger.js";
|
|
2
|
+
export declare function isThenable(value: unknown): value is PromiseLike<unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Reject, when it is piped, a plugin that could never work: one without a
|
|
5
|
+
* `process` function (it answered every matched request with a 500), or one
|
|
6
|
+
* whose `install`, `beforeRequest` or `onExchange` is a truthy non-function.
|
|
7
|
+
* The `install` and `beforeRequest` shapes already failed before this check;
|
|
8
|
+
* `onExchange` is new, so no working setup breaks.
|
|
9
|
+
*/
|
|
10
|
+
export declare function assertValidPlugin(plugin: unknown): asserts plugin is Schmock.Plugin;
|
|
11
|
+
/** Whether any plugin observes exchanges (a function `onExchange`). */
|
|
12
|
+
export declare function hasExchangeObserver(plugins: readonly Schmock.Plugin[]): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Report one settled exchange to each plugin's `onExchange`, in pipe order.
|
|
15
|
+
* Every observer gets its own frozen snapshot, built right before its call, so
|
|
16
|
+
* none sees or alters another's copy. A throwing or rejecting observer is
|
|
17
|
+
* logged and never stops the others. Reporting stops as soon as `isLive`
|
|
18
|
+
* returns false.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runExchangeHooks(input: {
|
|
21
|
+
plugins: readonly Schmock.Plugin[];
|
|
22
|
+
exchange: Schmock.Exchange;
|
|
23
|
+
logger: Pick<DebugLogger, "log">;
|
|
24
|
+
isLive?: () => boolean;
|
|
25
|
+
}): void;
|
|
26
|
+
/** The live reads a hook's instance forwards to the mock. */
|
|
27
|
+
export interface HookReadAccess {
|
|
28
|
+
history(method?: Schmock.HttpMethod, path?: string): Schmock.RequestRecord[];
|
|
29
|
+
called(method?: Schmock.HttpMethod, path?: string): boolean;
|
|
30
|
+
callCount(method?: Schmock.HttpMethod, path?: string): number;
|
|
31
|
+
lastRequest(method?: Schmock.HttpMethod, path?: string): Schmock.RequestRecord | undefined;
|
|
32
|
+
getRoutes(): Schmock.RouteInfo[];
|
|
33
|
+
getState(): Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
type RouteRegistrar = (route: Schmock.RouteKey, generator: Schmock.Generator, config: Schmock.RouteConfig) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Run a plugin's `install()` against an expiring facade that may register
|
|
38
|
+
* routes. A Promise returned from `install()` is rejected: the routes it would
|
|
39
|
+
* register later could not be rolled back. The caller owns the rollback of
|
|
40
|
+
* whatever the hook registered before it threw.
|
|
41
|
+
*/
|
|
42
|
+
export declare function runInstallHook(input: {
|
|
43
|
+
plugin: Schmock.Plugin;
|
|
44
|
+
reads: HookReadAccess;
|
|
45
|
+
registerRoute: RouteRegistrar;
|
|
46
|
+
logger: DebugLogger;
|
|
47
|
+
}): void;
|
|
48
|
+
/**
|
|
49
|
+
* Run `uninstall()` for each plugin, last piped first. A failing hook is
|
|
50
|
+
* logged and never stops the others.
|
|
51
|
+
*/
|
|
52
|
+
export declare function runUninstallHooks(input: {
|
|
53
|
+
plugins: readonly Schmock.Plugin[];
|
|
54
|
+
reads: HookReadAccess;
|
|
55
|
+
logger: DebugLogger;
|
|
56
|
+
}): void;
|
|
57
|
+
export {};
|