@dever-labs/mockly-driver 0.4.6
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/LICENSE +21 -0
- package/README.md +245 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +55 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +244 -0
- package/dist/install.js.map +1 -0
- package/dist/server.d.ts +90 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +224 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +15 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +52 -0
- package/dist/utils.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dever Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# mockly-driver
|
|
2
|
+
|
|
3
|
+
Node.js client for [Mockly](https://github.com/dever-labs/mockly) — start, stop, and control Mockly HTTP mock servers from Node.js test suites.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { MocklyServer } from 'mockly-driver'
|
|
7
|
+
|
|
8
|
+
const server = await MocklyServer.ensure() // install binary if needed, then start
|
|
9
|
+
|
|
10
|
+
await server.addMock({
|
|
11
|
+
id: 'get-users',
|
|
12
|
+
request: { method: 'GET', path: '/users' },
|
|
13
|
+
response: { status: 200, body: '[{"id":1}]', headers: { 'Content-Type': 'application/json' } },
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const res = await fetch(`${server.httpBase}/users`)
|
|
17
|
+
// → 200 [{"id":1}]
|
|
18
|
+
|
|
19
|
+
await server.stop()
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install --save-dev mockly-driver
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The Mockly binary is **not** bundled in the npm package. Download it once before running tests:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npx mockly-driver-install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or let `MocklyServer.ensure()` handle it automatically on first run.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### `MocklyServer.ensure(opts?)` _(recommended)_
|
|
41
|
+
|
|
42
|
+
Downloads the binary if missing, then starts the server.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const server = await MocklyServer.ensure()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `MocklyServer.create(opts?)`
|
|
49
|
+
|
|
50
|
+
Starts the server using an already-installed binary. Throws if the binary is not found.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const server = await MocklyServer.create()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Test framework integration
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// vitest / jest
|
|
60
|
+
import { MocklyServer } from 'mockly-driver'
|
|
61
|
+
|
|
62
|
+
let server: MocklyServer
|
|
63
|
+
|
|
64
|
+
beforeAll(async () => {
|
|
65
|
+
server = await MocklyServer.ensure()
|
|
66
|
+
}, 30_000)
|
|
67
|
+
|
|
68
|
+
afterAll(() => server?.stop())
|
|
69
|
+
|
|
70
|
+
beforeEach(() => server.reset()) // isolate each test
|
|
71
|
+
|
|
72
|
+
it('returns 200', async () => {
|
|
73
|
+
await server.addMock({
|
|
74
|
+
id: 'ping',
|
|
75
|
+
request: { method: 'GET', path: '/ping' },
|
|
76
|
+
response: { status: 200, body: 'pong' },
|
|
77
|
+
})
|
|
78
|
+
const res = await fetch(`${server.httpBase}/ping`)
|
|
79
|
+
expect(res.status).toBe(200)
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Management API
|
|
86
|
+
|
|
87
|
+
### `server.addMock(mock)`
|
|
88
|
+
|
|
89
|
+
Adds an HTTP mock. Mocks are matched in insertion order — first match wins.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
await server.addMock({
|
|
93
|
+
id: 'create-user',
|
|
94
|
+
request: {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
path: '/users',
|
|
97
|
+
headers: { Authorization: 'Bearer mytoken' }, // exact match
|
|
98
|
+
},
|
|
99
|
+
response: {
|
|
100
|
+
status: 201,
|
|
101
|
+
body: '{"id":42}',
|
|
102
|
+
headers: { 'Content-Type': 'application/json' },
|
|
103
|
+
delay: '50ms',
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> **Note:** Header matching is exact. Place mocks with header constraints _before_ less-specific fallbacks.
|
|
109
|
+
|
|
110
|
+
### `server.deleteMock(id)`
|
|
111
|
+
|
|
112
|
+
Removes a mock by id.
|
|
113
|
+
|
|
114
|
+
### `server.reset()`
|
|
115
|
+
|
|
116
|
+
Removes all dynamically added mocks, deactivates scenarios, and clears fault injection. Mocks defined in startup config are preserved. Call in `beforeEach` to keep tests isolated.
|
|
117
|
+
|
|
118
|
+
### Scenarios
|
|
119
|
+
|
|
120
|
+
Scenarios override mock responses when activated — useful for simulating outage modes.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const server = await MocklyServer.ensure({
|
|
124
|
+
scenarios: [
|
|
125
|
+
{
|
|
126
|
+
id: 'auth-down',
|
|
127
|
+
name: 'Auth service unavailable',
|
|
128
|
+
patches: [{ mock_id: 'token-endpoint', status: 503, body: '{"error":"down"}' }],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
await server.addMock({
|
|
134
|
+
id: 'token-endpoint',
|
|
135
|
+
request: { method: 'POST', path: '/token' },
|
|
136
|
+
response: { status: 200, body: '{"access_token":"abc"}' },
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Normal operation
|
|
140
|
+
const r1 = await fetch(`${server.httpBase}/token`, { method: 'POST' })
|
|
141
|
+
// → 200
|
|
142
|
+
|
|
143
|
+
await server.activateScenario('auth-down')
|
|
144
|
+
|
|
145
|
+
const r2 = await fetch(`${server.httpBase}/token`, { method: 'POST' })
|
|
146
|
+
// → 503
|
|
147
|
+
|
|
148
|
+
await server.deactivateScenario('auth-down')
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Fault injection
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// Delay every response by 200 ms
|
|
155
|
+
await server.setFault({ enabled: true, delay: '200ms' })
|
|
156
|
+
|
|
157
|
+
// Override all responses with 503
|
|
158
|
+
await server.setFault({ enabled: true, status_override: 503 })
|
|
159
|
+
|
|
160
|
+
// Randomly fail 30% of requests
|
|
161
|
+
await server.setFault({ enabled: true, status_override: 503, error_rate: 0.3 })
|
|
162
|
+
|
|
163
|
+
await server.clearFault()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Binary installation
|
|
169
|
+
|
|
170
|
+
### Default (download from GitHub)
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
npx mockly-driver-install
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Or programmatically:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { install } from 'mockly-driver'
|
|
180
|
+
await install()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Artifactory / internal mirror
|
|
184
|
+
|
|
185
|
+
Set `MOCKLY_DOWNLOAD_BASE_URL` to your mirror's base URL (the path up to and including the version segment prefix):
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
MOCKLY_DOWNLOAD_BASE_URL=https://artifactory.company.com/artifactory/github-releases/dever-labs/mockly/releases/download \
|
|
189
|
+
npx mockly-driver-install
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Artifactory setup: create a **Generic Remote Repository** pointing to `https://github.com` and enable "Store Artifacts Locally". The download URL then becomes `https://<artifactory>/artifactory/<repo>/dever-labs/mockly/releases/download`.
|
|
193
|
+
|
|
194
|
+
### HTTP / HTTPS proxy
|
|
195
|
+
|
|
196
|
+
Set `HTTPS_PROXY` or `HTTP_PROXY` before running the install:
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
HTTPS_PROXY=https://proxy.company.com:3128 npx mockly-driver-install
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Proxy authentication is supported via the proxy URL: `https://user:pass@proxy:3128`.
|
|
203
|
+
|
|
204
|
+
> **Note:** If your proxy username or password contains special characters (e.g. `@`, `:`, `/`), URL-encode them first — e.g. `p@ss` → `p%40ss`. Use `encodeURIComponent()` in Node.js or an online encoder.
|
|
205
|
+
|
|
206
|
+
> **Tip:** For Artifactory, `MOCKLY_DOWNLOAD_BASE_URL` is simpler and more reliable than `HTTPS_PROXY`.
|
|
207
|
+
|
|
208
|
+
### Air-gapped environments
|
|
209
|
+
|
|
210
|
+
Pre-stage the binary and point to it:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
# On a machine with internet access:
|
|
214
|
+
npx mockly-driver-install
|
|
215
|
+
|
|
216
|
+
# Copy bin/mockly[.exe] to the air-gapped machine, then:
|
|
217
|
+
MOCKLY_BINARY_PATH=/opt/tools/mockly npx vitest run
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Or set `MOCKLY_NO_INSTALL=true` to make the binary absence a hard error with actionable instructions:
|
|
221
|
+
|
|
222
|
+
```sh
|
|
223
|
+
MOCKLY_NO_INSTALL=true npx vitest run # fails fast if binary not staged
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Environment variable reference
|
|
227
|
+
|
|
228
|
+
| Variable | Description |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `MOCKLY_BINARY_PATH` | Absolute path to a pre-existing binary. Skips all download logic. |
|
|
231
|
+
| `MOCKLY_DOWNLOAD_BASE_URL` | Base URL override for binary downloads (Artifactory / mirrors). |
|
|
232
|
+
| `MOCKLY_VERSION` | Binary version to install. Default: `v0.1.0`. |
|
|
233
|
+
| `MOCKLY_NO_INSTALL` | If set, fail with instructions instead of downloading. |
|
|
234
|
+
| `HTTPS_PROXY` / `HTTP_PROXY` | Route downloads through an HTTP proxy (supports CONNECT). |
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Requirements
|
|
239
|
+
|
|
240
|
+
- Node.js ≥ 18
|
|
241
|
+
- Platforms: Linux (x64/arm64), macOS (x64/arm64), Windows (x64)
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { MocklyServer } from './server.js';
|
|
2
|
+
export { install, getBinaryPath, DEFAULT_MOCKLY_VERSION } from './install.js';
|
|
3
|
+
export { getFreePort } from './utils.js';
|
|
4
|
+
export type { HttpMock, MockRequest, MockResponse, Scenario, ScenarioPatch, FaultConfig, MocklyServerOptions, } from './types.js';
|
|
5
|
+
export type { InstallOptions } from './install.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,YAAY,EACV,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,WAAW,EACX,mBAAmB,GACpB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Version of the Mockly binary this package was tested against. */
|
|
2
|
+
export declare const DEFAULT_MOCKLY_VERSION = "v0.1.0";
|
|
3
|
+
export interface InstallOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Mockly version to install.
|
|
6
|
+
* @default process.env.MOCKLY_VERSION ?? DEFAULT_MOCKLY_VERSION
|
|
7
|
+
*/
|
|
8
|
+
version?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Base URL for downloading release assets.
|
|
11
|
+
*
|
|
12
|
+
* Override this to route downloads through Artifactory or an internal mirror:
|
|
13
|
+
* ```
|
|
14
|
+
* MOCKLY_DOWNLOAD_BASE_URL=https://artifactory.company.com/artifactory/github-releases/dever-labs/mockly/releases/download
|
|
15
|
+
* ```
|
|
16
|
+
* The full download URL becomes `${baseUrl}/${version}/${assetName}`.
|
|
17
|
+
*
|
|
18
|
+
* @default process.env.MOCKLY_DOWNLOAD_BASE_URL ?? GitHub releases URL
|
|
19
|
+
*/
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Directory to place the downloaded binary.
|
|
23
|
+
* @default path.join(process.cwd(), 'bin')
|
|
24
|
+
*/
|
|
25
|
+
binDir?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Re-download even if the binary already exists.
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
force?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns the path to the installed binary, or `null` if not found.
|
|
34
|
+
*
|
|
35
|
+
* Resolution order:
|
|
36
|
+
* 1. `MOCKLY_BINARY_PATH` env var (absolute path to pre-staged binary)
|
|
37
|
+
* 2. `<binDir>/mockly[.exe]` (downloaded by `install()`)
|
|
38
|
+
* 3. `<cwd>/node_modules/.bin/mockly[.exe]` (future: native npm wrapper)
|
|
39
|
+
*/
|
|
40
|
+
export declare function getBinaryPath(binDir?: string): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* Downloads (or locates) the Mockly binary and returns its path.
|
|
43
|
+
*
|
|
44
|
+
* Environment variables (all optional):
|
|
45
|
+
* - `MOCKLY_BINARY_PATH` — use this exact binary; skips all download logic
|
|
46
|
+
* - `MOCKLY_NO_INSTALL` — throw instead of downloading (for air-gapped envs
|
|
47
|
+
* where the binary is staged externally)
|
|
48
|
+
* - `MOCKLY_DOWNLOAD_BASE_URL` — base URL override for Artifactory / internal mirrors
|
|
49
|
+
* - `MOCKLY_VERSION` — binary version override
|
|
50
|
+
* - `HTTPS_PROXY` / `HTTP_PROXY` — route the download through an HTTP proxy
|
|
51
|
+
*
|
|
52
|
+
* @returns Absolute path to the installed binary.
|
|
53
|
+
*/
|
|
54
|
+
export declare function install(opts?: InstallOptions): Promise<string>;
|
|
55
|
+
//# sourceMappingURL=install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAOA,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,WAAW,CAAA;AAK9C,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoB5D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,OAAO,CAAC,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyDxE"}
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import tls from 'tls';
|
|
4
|
+
import { createWriteStream, existsSync, mkdirSync, chmodSync } from 'fs';
|
|
5
|
+
import { join, resolve, dirname } from 'path';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
/** Version of the Mockly binary this package was tested against. */
|
|
8
|
+
export const DEFAULT_MOCKLY_VERSION = 'v0.1.0';
|
|
9
|
+
/** Default GitHub releases base URL. */
|
|
10
|
+
const GITHUB_BASE = 'https://github.com/dever-labs/mockly/releases/download';
|
|
11
|
+
/**
|
|
12
|
+
* Returns the path to the installed binary, or `null` if not found.
|
|
13
|
+
*
|
|
14
|
+
* Resolution order:
|
|
15
|
+
* 1. `MOCKLY_BINARY_PATH` env var (absolute path to pre-staged binary)
|
|
16
|
+
* 2. `<binDir>/mockly[.exe]` (downloaded by `install()`)
|
|
17
|
+
* 3. `<cwd>/node_modules/.bin/mockly[.exe]` (future: native npm wrapper)
|
|
18
|
+
*/
|
|
19
|
+
export function getBinaryPath(binDir) {
|
|
20
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
21
|
+
if (process.env.MOCKLY_BINARY_PATH) {
|
|
22
|
+
const p = resolve(process.env.MOCKLY_BINARY_PATH);
|
|
23
|
+
if (existsSync(p))
|
|
24
|
+
return p;
|
|
25
|
+
}
|
|
26
|
+
// Bundled binary from platform sub-package (installed via optionalDependencies).
|
|
27
|
+
const bundled = getBundledBinaryPath(ext);
|
|
28
|
+
if (bundled)
|
|
29
|
+
return bundled;
|
|
30
|
+
const dir = binDir ?? join(process.cwd(), 'bin');
|
|
31
|
+
const fromBinDir = join(dir, `mockly${ext}`);
|
|
32
|
+
if (existsSync(fromBinDir))
|
|
33
|
+
return fromBinDir;
|
|
34
|
+
const fromModules = resolve(process.cwd(), 'node_modules', '.bin', `mockly${ext}`);
|
|
35
|
+
if (existsSync(fromModules))
|
|
36
|
+
return fromModules;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Downloads (or locates) the Mockly binary and returns its path.
|
|
41
|
+
*
|
|
42
|
+
* Environment variables (all optional):
|
|
43
|
+
* - `MOCKLY_BINARY_PATH` — use this exact binary; skips all download logic
|
|
44
|
+
* - `MOCKLY_NO_INSTALL` — throw instead of downloading (for air-gapped envs
|
|
45
|
+
* where the binary is staged externally)
|
|
46
|
+
* - `MOCKLY_DOWNLOAD_BASE_URL` — base URL override for Artifactory / internal mirrors
|
|
47
|
+
* - `MOCKLY_VERSION` — binary version override
|
|
48
|
+
* - `HTTPS_PROXY` / `HTTP_PROXY` — route the download through an HTTP proxy
|
|
49
|
+
*
|
|
50
|
+
* @returns Absolute path to the installed binary.
|
|
51
|
+
*/
|
|
52
|
+
export async function install(opts = {}) {
|
|
53
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
54
|
+
const binDir = opts.binDir ?? join(process.cwd(), 'bin');
|
|
55
|
+
const binPath = join(binDir, `mockly${ext}`);
|
|
56
|
+
// 1. MOCKLY_BINARY_PATH — use a pre-staged binary without downloading
|
|
57
|
+
if (process.env.MOCKLY_BINARY_PATH) {
|
|
58
|
+
const staged = resolve(process.env.MOCKLY_BINARY_PATH);
|
|
59
|
+
if (!existsSync(staged)) {
|
|
60
|
+
throw new Error(`MOCKLY_BINARY_PATH is set to "${staged}" but the file does not exist.\n` +
|
|
61
|
+
`Stage the binary for platform "${process.platform}/${process.arch}" before running tests.`);
|
|
62
|
+
}
|
|
63
|
+
return staged;
|
|
64
|
+
}
|
|
65
|
+
// 2. Bundled binary via optionalDependencies — no download needed.
|
|
66
|
+
const bundled = getBundledBinaryPath(ext);
|
|
67
|
+
if (bundled && !opts.force)
|
|
68
|
+
return bundled;
|
|
69
|
+
// 3. Already installed — skip unless force
|
|
70
|
+
if (!opts.force && existsSync(binPath)) {
|
|
71
|
+
return binPath;
|
|
72
|
+
}
|
|
73
|
+
// 4. MOCKLY_NO_INSTALL — fail fast with actionable instructions
|
|
74
|
+
if (process.env.MOCKLY_NO_INSTALL) {
|
|
75
|
+
throw new Error(`MOCKLY_NO_INSTALL is set but no Mockly binary was found.\n\n` +
|
|
76
|
+
`To resolve this, choose one of:\n` +
|
|
77
|
+
` a) Stage the binary manually:\n` +
|
|
78
|
+
` Place the binary at: ${binPath}\n` +
|
|
79
|
+
` Download from: https://github.com/dever-labs/mockly/releases\n\n` +
|
|
80
|
+
` b) Set MOCKLY_BINARY_PATH to the absolute path of an existing binary.\n\n` +
|
|
81
|
+
` c) Use MOCKLY_DOWNLOAD_BASE_URL to point to an internal mirror:\n` +
|
|
82
|
+
` MOCKLY_DOWNLOAD_BASE_URL=https://artifactory.company.com/artifactory/github-releases/dever-labs/mockly/releases/download`);
|
|
83
|
+
}
|
|
84
|
+
// 5. Download
|
|
85
|
+
const version = opts.version ?? process.env.MOCKLY_VERSION ?? DEFAULT_MOCKLY_VERSION;
|
|
86
|
+
const baseUrl = opts.baseUrl ?? process.env.MOCKLY_DOWNLOAD_BASE_URL ?? GITHUB_BASE;
|
|
87
|
+
const asset = getAssetName();
|
|
88
|
+
const url = `${baseUrl}/${version}/${asset}`;
|
|
89
|
+
mkdirSync(binDir, { recursive: true });
|
|
90
|
+
console.log(`mockly-node: downloading ${asset} from ${url}`);
|
|
91
|
+
await downloadFile(url, binPath);
|
|
92
|
+
if (process.platform !== 'win32') {
|
|
93
|
+
chmodSync(binPath, 0o755);
|
|
94
|
+
}
|
|
95
|
+
console.log(`mockly-node: installed at ${binPath}`);
|
|
96
|
+
return binPath;
|
|
97
|
+
}
|
|
98
|
+
/** Maps `${process.platform}-${process.arch}` to the platform sub-package name. */
|
|
99
|
+
const PLATFORM_PACKAGES = {
|
|
100
|
+
'linux-x64': '@dever-labs/mockly-driver-linux-x64',
|
|
101
|
+
'linux-arm64': '@dever-labs/mockly-driver-linux-arm64',
|
|
102
|
+
'darwin-x64': '@dever-labs/mockly-driver-darwin-x64',
|
|
103
|
+
'darwin-arm64': '@dever-labs/mockly-driver-darwin-arm64',
|
|
104
|
+
'win32-x64': '@dever-labs/mockly-driver-win32-x64',
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Tries to find the mockly binary bundled in the matching platform sub-package.
|
|
108
|
+
* Returns the path if found, null otherwise.
|
|
109
|
+
*/
|
|
110
|
+
function getBundledBinaryPath(ext) {
|
|
111
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
112
|
+
const pkgName = PLATFORM_PACKAGES[platformKey];
|
|
113
|
+
if (!pkgName)
|
|
114
|
+
return null;
|
|
115
|
+
try {
|
|
116
|
+
const req = createRequire(import.meta.url);
|
|
117
|
+
const pkgJsonPath = req.resolve(`${pkgName}/package.json`);
|
|
118
|
+
const candidate = join(dirname(pkgJsonPath), 'bin', `mockly${ext}`);
|
|
119
|
+
if (existsSync(candidate))
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// optional dep not installed — fall through to download
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const ARCH_MAP = {
|
|
128
|
+
x64: 'amd64',
|
|
129
|
+
arm64: 'arm64',
|
|
130
|
+
};
|
|
131
|
+
function getAssetName() {
|
|
132
|
+
const os = process.platform === 'win32' ? 'windows'
|
|
133
|
+
: process.platform === 'darwin' ? 'darwin'
|
|
134
|
+
: 'linux';
|
|
135
|
+
const arch = ARCH_MAP[process.arch];
|
|
136
|
+
if (!arch) {
|
|
137
|
+
throw new Error(`Unsupported architecture: ${process.arch}.\n` +
|
|
138
|
+
`Supported: x64 (amd64), arm64.\n` +
|
|
139
|
+
`For other platforms, build from source: https://github.com/dever-labs/mockly`);
|
|
140
|
+
}
|
|
141
|
+
return `mockly-${os}-${arch}${process.platform === 'win32' ? '.exe' : ''}`;
|
|
142
|
+
}
|
|
143
|
+
// ─── Download ─────────────────────────────────────────────────────────────────
|
|
144
|
+
function getProxyUrl() {
|
|
145
|
+
return (process.env.HTTPS_PROXY ??
|
|
146
|
+
process.env.https_proxy ??
|
|
147
|
+
process.env.npm_config_https_proxy ??
|
|
148
|
+
process.env.HTTP_PROXY ??
|
|
149
|
+
process.env.http_proxy ??
|
|
150
|
+
process.env.npm_config_proxy);
|
|
151
|
+
}
|
|
152
|
+
function downloadFile(url, dest) {
|
|
153
|
+
const proxyUrl = getProxyUrl();
|
|
154
|
+
return proxyUrl ? downloadViaProxy(url, dest, proxyUrl) : downloadDirect(url, dest);
|
|
155
|
+
}
|
|
156
|
+
function downloadDirect(url, dest) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const get = (u) => {
|
|
159
|
+
https.get(u, { headers: { 'User-Agent': 'mockly-install' } }, (res) => {
|
|
160
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
161
|
+
if (!res.headers.location) {
|
|
162
|
+
reject(new Error(`HTTP ${res.statusCode} from ${u} — redirect with no Location header`));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
get(res.headers.location);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (res.statusCode !== 200) {
|
|
169
|
+
reject(new Error(`HTTP ${res.statusCode} from ${u}`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const ws = createWriteStream(dest);
|
|
173
|
+
res.pipe(ws);
|
|
174
|
+
ws.on('finish', resolve);
|
|
175
|
+
ws.on('error', reject);
|
|
176
|
+
}).on('error', reject);
|
|
177
|
+
};
|
|
178
|
+
get(url);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Downloads via HTTP CONNECT proxy (for environments where HTTPS_PROXY /
|
|
183
|
+
* HTTP_PROXY is set but MOCKLY_DOWNLOAD_BASE_URL cannot be used).
|
|
184
|
+
*
|
|
185
|
+
* For Artifactory, prefer setting MOCKLY_DOWNLOAD_BASE_URL instead — it is
|
|
186
|
+
* simpler and avoids CONNECT tunnel complexity.
|
|
187
|
+
*/
|
|
188
|
+
function downloadViaProxy(targetUrl, dest, proxyUrl) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const target = new URL(targetUrl);
|
|
191
|
+
const proxy = new URL(proxyUrl);
|
|
192
|
+
const connectOpts = {
|
|
193
|
+
host: proxy.hostname,
|
|
194
|
+
port: parseInt(proxy.port) || 3128,
|
|
195
|
+
method: 'CONNECT',
|
|
196
|
+
path: `${target.hostname}:443`,
|
|
197
|
+
headers: {},
|
|
198
|
+
};
|
|
199
|
+
if (proxy.username) {
|
|
200
|
+
const password = proxy.password || '';
|
|
201
|
+
const auth = Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(password)}`).toString('base64');
|
|
202
|
+
connectOpts.headers['Proxy-Authorization'] = `Basic ${auth}`;
|
|
203
|
+
}
|
|
204
|
+
const connectReq = http.request(connectOpts);
|
|
205
|
+
connectReq.on('connect', (_res, socket) => {
|
|
206
|
+
// Wrap the plain socket in TLS to complete the HTTPS tunnel
|
|
207
|
+
const tlsSocket = tls.connect({ socket, servername: target.hostname });
|
|
208
|
+
const req = https.request({
|
|
209
|
+
createConnection: () => tlsSocket,
|
|
210
|
+
hostname: target.hostname,
|
|
211
|
+
path: target.pathname + target.search,
|
|
212
|
+
method: 'GET',
|
|
213
|
+
headers: { 'User-Agent': 'mockly-install' },
|
|
214
|
+
}, (res) => {
|
|
215
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
216
|
+
if (!res.headers.location) {
|
|
217
|
+
tlsSocket.destroy();
|
|
218
|
+
reject(new Error(`HTTP ${res.statusCode} from ${targetUrl} — redirect with no Location header`));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
tlsSocket.destroy();
|
|
222
|
+
downloadViaProxy(res.headers.location, dest, proxyUrl).then(resolve).catch(reject);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (res.statusCode !== 200) {
|
|
226
|
+
reject(new Error(`HTTP ${res.statusCode} from ${targetUrl} (via proxy ${proxyUrl})`));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const ws = createWriteStream(dest);
|
|
230
|
+
res.pipe(ws);
|
|
231
|
+
ws.on('finish', resolve);
|
|
232
|
+
ws.on('error', (err) => {
|
|
233
|
+
tlsSocket.destroy();
|
|
234
|
+
reject(err);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
req.on('error', reject);
|
|
238
|
+
req.end();
|
|
239
|
+
});
|
|
240
|
+
connectReq.on('error', reject);
|
|
241
|
+
connectReq.end();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { HttpMock, FaultConfig, MocklyServerOptions } from './types.js';
|
|
2
|
+
import type { InstallOptions } from './install.js';
|
|
3
|
+
/**
|
|
4
|
+
* Controls a Mockly server process for use in integration tests.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* // Recommended: ensure binary is installed, then start
|
|
9
|
+
* const server = await MocklyServer.ensure()
|
|
10
|
+
*
|
|
11
|
+
* await server.addMock({
|
|
12
|
+
* id: 'get-users',
|
|
13
|
+
* request: { method: 'GET', path: '/users' },
|
|
14
|
+
* response: { status: 200, body: '[{"id":1}]', headers: { 'Content-Type': 'application/json' } },
|
|
15
|
+
* })
|
|
16
|
+
*
|
|
17
|
+
* // Point your HTTP client at server.httpBase
|
|
18
|
+
* const res = await fetch(`${server.httpBase}/users`)
|
|
19
|
+
*
|
|
20
|
+
* await server.stop()
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare class MocklyServer {
|
|
24
|
+
readonly httpPort: number;
|
|
25
|
+
readonly apiPort: number;
|
|
26
|
+
private proc;
|
|
27
|
+
private constructor();
|
|
28
|
+
/** Base URL of the HTTP mock server — e.g. `http://127.0.0.1:45123` */
|
|
29
|
+
get httpBase(): string;
|
|
30
|
+
/** Base URL of the management API — e.g. `http://127.0.0.1:45124` */
|
|
31
|
+
get apiBase(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Installs the Mockly binary if it is not already present, then starts the
|
|
34
|
+
* server. This is the recommended entry point for most test setups.
|
|
35
|
+
*
|
|
36
|
+
* Respects all `InstallOptions` and their corresponding environment
|
|
37
|
+
* variables — see {@link install} for details.
|
|
38
|
+
*/
|
|
39
|
+
static ensure(opts?: MocklyServerOptions & InstallOptions): Promise<MocklyServer>;
|
|
40
|
+
/**
|
|
41
|
+
* Starts the server using an already-installed binary.
|
|
42
|
+
* Throws immediately if the binary cannot be found — call `ensure()` instead
|
|
43
|
+
* if you want automatic installation.
|
|
44
|
+
*
|
|
45
|
+
* Ports are allocated atomically (both held open simultaneously) to avoid
|
|
46
|
+
* TOCTOU races where another process could claim a port between allocations.
|
|
47
|
+
* If startup fails due to a port conflict, create() retries up to 3 times
|
|
48
|
+
* with freshly allocated ports before giving up.
|
|
49
|
+
*/
|
|
50
|
+
static create(opts?: MocklyServerOptions): Promise<MocklyServer>;
|
|
51
|
+
/** Kills the Mockly process and waits for it to exit. */
|
|
52
|
+
stop(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Registers a new HTTP mock via the management API.
|
|
55
|
+
* Mocks are matched in insertion order — the first match wins.
|
|
56
|
+
*
|
|
57
|
+
* **Header matching** uses exact string comparison.
|
|
58
|
+
* Place more-specific mocks (with header requirements) before less-specific
|
|
59
|
+
* fallbacks to ensure correct priority.
|
|
60
|
+
*/
|
|
61
|
+
addMock(mock: HttpMock): Promise<void>;
|
|
62
|
+
/** Removes a mock by id. */
|
|
63
|
+
deleteMock(id: string): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Activates a named scenario, patching the responses of referenced mocks.
|
|
66
|
+
* The scenario must have been declared in `MocklyServerOptions.scenarios`.
|
|
67
|
+
*/
|
|
68
|
+
activateScenario(id: string): Promise<void>;
|
|
69
|
+
/** Deactivates a previously activated scenario. */
|
|
70
|
+
deactivateScenario(id: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Enables global fault injection.
|
|
73
|
+
* Faults apply to every request regardless of mock matching.
|
|
74
|
+
*/
|
|
75
|
+
setFault(config: FaultConfig): Promise<void>;
|
|
76
|
+
/** Disables all active fault injection. */
|
|
77
|
+
clearFault(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Resets all state: removes dynamically added mocks, deactivates scenarios,
|
|
80
|
+
* and clears fault injection. Mocks from the startup config are preserved.
|
|
81
|
+
*
|
|
82
|
+
* Call this in `beforeEach` to keep tests isolated.
|
|
83
|
+
*/
|
|
84
|
+
reset(): Promise<void>;
|
|
85
|
+
private _start;
|
|
86
|
+
private _writeConfig;
|
|
87
|
+
private _waitReady;
|
|
88
|
+
private _post;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAY,WAAW,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAIlD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,YAAY;IAIrB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM;IAJ1B,OAAO,CAAC,IAAI,CAA4B;IAExC,OAAO;IAKP,uEAAuE;IACvE,IAAI,QAAQ,IAAI,MAAM,CAA+C;IAErE,qEAAqE;IACrE,IAAI,OAAO,IAAI,MAAM,CAA8C;IAEnE;;;;;;OAMG;WACU,MAAM,CAAC,IAAI,GAAE,mBAAmB,GAAG,cAAmB,GAAG,OAAO,CAAC,YAAY,CAAC;IAO3F;;;;;;;;;OASG;WACU,MAAM,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC;IAwB1E,yDAAyD;IACnD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAU3B;;;;;;;OAOG;IACG,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAK5C,4BAA4B;IACtB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C;;;OAGG;IACG,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjD,mDAAmD;IAC7C,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnD;;;OAGG;IACG,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlD,2CAA2C;IACrC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAMd,MAAM;IA2BpB,OAAO,CAAC,YAAY;YA4BN,UAAU;IAcxB,OAAO,CAAC,KAAK;CAOd"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import { install, getBinaryPath } from './install.js';
|
|
7
|
+
import { getFreePorts, sleep } from './utils.js';
|
|
8
|
+
/**
|
|
9
|
+
* Controls a Mockly server process for use in integration tests.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // Recommended: ensure binary is installed, then start
|
|
14
|
+
* const server = await MocklyServer.ensure()
|
|
15
|
+
*
|
|
16
|
+
* await server.addMock({
|
|
17
|
+
* id: 'get-users',
|
|
18
|
+
* request: { method: 'GET', path: '/users' },
|
|
19
|
+
* response: { status: 200, body: '[{"id":1}]', headers: { 'Content-Type': 'application/json' } },
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* // Point your HTTP client at server.httpBase
|
|
23
|
+
* const res = await fetch(`${server.httpBase}/users`)
|
|
24
|
+
*
|
|
25
|
+
* await server.stop()
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class MocklyServer {
|
|
29
|
+
httpPort;
|
|
30
|
+
apiPort;
|
|
31
|
+
proc = null;
|
|
32
|
+
constructor(httpPort, apiPort) {
|
|
33
|
+
this.httpPort = httpPort;
|
|
34
|
+
this.apiPort = apiPort;
|
|
35
|
+
}
|
|
36
|
+
/** Base URL of the HTTP mock server — e.g. `http://127.0.0.1:45123` */
|
|
37
|
+
get httpBase() { return `http://127.0.0.1:${this.httpPort}`; }
|
|
38
|
+
/** Base URL of the management API — e.g. `http://127.0.0.1:45124` */
|
|
39
|
+
get apiBase() { return `http://127.0.0.1:${this.apiPort}`; }
|
|
40
|
+
/**
|
|
41
|
+
* Installs the Mockly binary if it is not already present, then starts the
|
|
42
|
+
* server. This is the recommended entry point for most test setups.
|
|
43
|
+
*
|
|
44
|
+
* Respects all `InstallOptions` and their corresponding environment
|
|
45
|
+
* variables — see {@link install} for details.
|
|
46
|
+
*/
|
|
47
|
+
static async ensure(opts = {}) {
|
|
48
|
+
if (!getBinaryPath(opts.binDir)) {
|
|
49
|
+
await install(opts);
|
|
50
|
+
}
|
|
51
|
+
return MocklyServer.create(opts);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Starts the server using an already-installed binary.
|
|
55
|
+
* Throws immediately if the binary cannot be found — call `ensure()` instead
|
|
56
|
+
* if you want automatic installation.
|
|
57
|
+
*
|
|
58
|
+
* Ports are allocated atomically (both held open simultaneously) to avoid
|
|
59
|
+
* TOCTOU races where another process could claim a port between allocations.
|
|
60
|
+
* If startup fails due to a port conflict, create() retries up to 3 times
|
|
61
|
+
* with freshly allocated ports before giving up.
|
|
62
|
+
*/
|
|
63
|
+
static async create(opts = {}) {
|
|
64
|
+
const MAX_ATTEMPTS = 3;
|
|
65
|
+
let lastError;
|
|
66
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
67
|
+
const [httpPort, apiPort] = await getFreePorts(2);
|
|
68
|
+
const server = new MocklyServer(httpPort, apiPort);
|
|
69
|
+
try {
|
|
70
|
+
await server._start(opts.scenarios ?? []);
|
|
71
|
+
return server;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
await server.stop();
|
|
75
|
+
const msg = err.message;
|
|
76
|
+
if (isPortConflict(msg) && attempt < MAX_ATTEMPTS - 1) {
|
|
77
|
+
lastError = err;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
throw lastError ?? new Error('Failed to start Mockly after multiple port allocation attempts');
|
|
84
|
+
}
|
|
85
|
+
/** Kills the Mockly process and waits for it to exit. */
|
|
86
|
+
async stop() {
|
|
87
|
+
if (this.proc) {
|
|
88
|
+
this.proc.kill();
|
|
89
|
+
await new Promise((r) => this.proc.once('exit', r));
|
|
90
|
+
this.proc = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ── Management API ──────────────────────────────────────────────────────────
|
|
94
|
+
/**
|
|
95
|
+
* Registers a new HTTP mock via the management API.
|
|
96
|
+
* Mocks are matched in insertion order — the first match wins.
|
|
97
|
+
*
|
|
98
|
+
* **Header matching** uses exact string comparison.
|
|
99
|
+
* Place more-specific mocks (with header requirements) before less-specific
|
|
100
|
+
* fallbacks to ensure correct priority.
|
|
101
|
+
*/
|
|
102
|
+
async addMock(mock) {
|
|
103
|
+
const res = await this._post('/api/mocks/http', mock);
|
|
104
|
+
if (!res.ok)
|
|
105
|
+
throw new Error(`addMock(${mock.id}) failed: HTTP ${res.status}`);
|
|
106
|
+
}
|
|
107
|
+
/** Removes a mock by id. */
|
|
108
|
+
async deleteMock(id) {
|
|
109
|
+
await fetch(`${this.apiBase}/api/mocks/http/${id}`, { method: 'DELETE' });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Activates a named scenario, patching the responses of referenced mocks.
|
|
113
|
+
* The scenario must have been declared in `MocklyServerOptions.scenarios`.
|
|
114
|
+
*/
|
|
115
|
+
async activateScenario(id) {
|
|
116
|
+
const res = await this._post(`/api/scenarios/${id}/activate`, null);
|
|
117
|
+
if (!res.ok)
|
|
118
|
+
throw new Error(`activateScenario(${id}) failed: HTTP ${res.status}`);
|
|
119
|
+
}
|
|
120
|
+
/** Deactivates a previously activated scenario. */
|
|
121
|
+
async deactivateScenario(id) {
|
|
122
|
+
const res = await fetch(`${this.apiBase}/api/scenarios/${id}/activate`, { method: 'DELETE' });
|
|
123
|
+
if (!res.ok)
|
|
124
|
+
throw new Error(`deactivateScenario(${id}) failed: HTTP ${res.status}`);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Enables global fault injection.
|
|
128
|
+
* Faults apply to every request regardless of mock matching.
|
|
129
|
+
*/
|
|
130
|
+
async setFault(config) {
|
|
131
|
+
const res = await this._post('/api/fault', config);
|
|
132
|
+
if (!res.ok)
|
|
133
|
+
throw new Error(`setFault failed: HTTP ${res.status}`);
|
|
134
|
+
}
|
|
135
|
+
/** Disables all active fault injection. */
|
|
136
|
+
async clearFault() {
|
|
137
|
+
await fetch(`${this.apiBase}/api/fault`, { method: 'DELETE' });
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Resets all state: removes dynamically added mocks, deactivates scenarios,
|
|
141
|
+
* and clears fault injection. Mocks from the startup config are preserved.
|
|
142
|
+
*
|
|
143
|
+
* Call this in `beforeEach` to keep tests isolated.
|
|
144
|
+
*/
|
|
145
|
+
async reset() {
|
|
146
|
+
await this._post('/api/reset', null);
|
|
147
|
+
}
|
|
148
|
+
// ── Private ─────────────────────────────────────────────────────────────────
|
|
149
|
+
async _start(scenarios) {
|
|
150
|
+
const bin = getBinaryPath();
|
|
151
|
+
if (!bin) {
|
|
152
|
+
throw new Error('Mockly binary not found. Use `MocklyServer.ensure()` instead of ' +
|
|
153
|
+
'`MocklyServer.create()` to install automatically, or call `install()` first.\n' +
|
|
154
|
+
'For pre-staged binaries set MOCKLY_BINARY_PATH to the absolute path.');
|
|
155
|
+
}
|
|
156
|
+
const cfgPath = this._writeConfig(scenarios);
|
|
157
|
+
let stderrOutput = '';
|
|
158
|
+
this.proc = spawn(bin, ['start', '--config', cfgPath, `--api-port=${this.apiPort}`], {
|
|
159
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
160
|
+
});
|
|
161
|
+
this.proc.stderr?.on('data', (chunk) => { stderrOutput += chunk.toString(); });
|
|
162
|
+
this.proc.on('error', (err) => { throw new Error(`mockly spawn error: ${err.message}`); });
|
|
163
|
+
try {
|
|
164
|
+
await this._waitReady();
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
const detail = stderrOutput.trim() ? `\nMockly output:\n${stderrOutput.trim()}` : '';
|
|
168
|
+
throw new Error(`${err.message}${detail}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
_writeConfig(scenarios) {
|
|
172
|
+
const dir = join(tmpdir(), `mockly-node-${Date.now()}`);
|
|
173
|
+
mkdirSync(dir, { recursive: true });
|
|
174
|
+
const cfgPath = join(dir, 'mockly.yaml');
|
|
175
|
+
const config = {
|
|
176
|
+
mockly: { api: { port: this.apiPort } },
|
|
177
|
+
protocols: { http: { enabled: true, port: this.httpPort } },
|
|
178
|
+
};
|
|
179
|
+
if (scenarios.length > 0) {
|
|
180
|
+
config.scenarios = scenarios.map((s) => ({
|
|
181
|
+
id: s.id,
|
|
182
|
+
name: s.name,
|
|
183
|
+
patches: s.patches.map((p) => {
|
|
184
|
+
const patch = { mock_id: p.mock_id };
|
|
185
|
+
if (p.status !== undefined)
|
|
186
|
+
patch.status = p.status;
|
|
187
|
+
if (p.body !== undefined)
|
|
188
|
+
patch.body = p.body;
|
|
189
|
+
if (p.delay !== undefined)
|
|
190
|
+
patch.delay = p.delay;
|
|
191
|
+
return patch;
|
|
192
|
+
}),
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
writeFileSync(cfgPath, yaml.dump(config), 'utf-8');
|
|
196
|
+
return cfgPath;
|
|
197
|
+
}
|
|
198
|
+
async _waitReady(maxMs = 10_000) {
|
|
199
|
+
const deadline = Date.now() + maxMs;
|
|
200
|
+
while (Date.now() < deadline) {
|
|
201
|
+
try {
|
|
202
|
+
const res = await fetch(`${this.apiBase}/api/protocols`, {
|
|
203
|
+
signal: AbortSignal.timeout(300),
|
|
204
|
+
});
|
|
205
|
+
if (res.ok)
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
catch { /* not ready yet */ }
|
|
209
|
+
await sleep(50);
|
|
210
|
+
}
|
|
211
|
+
throw new Error(`Mockly did not become ready on port ${this.apiPort} within ${maxMs}ms`);
|
|
212
|
+
}
|
|
213
|
+
_post(path, body) {
|
|
214
|
+
return fetch(`${this.apiBase}${path}`, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: body !== null ? { 'Content-Type': 'application/json' } : {},
|
|
217
|
+
body: body !== null ? JSON.stringify(body) : undefined,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function isPortConflict(errorMessage) {
|
|
222
|
+
return /address already in use|EADDRINUSE|bind/i.test(errorMessage);
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Request matching criteria for an HTTP mock. */
|
|
2
|
+
export interface MockRequest {
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
/** Exact header value matching — e.g. `{ Authorization: 'Bearer token123' }` */
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/** Response definition for an HTTP mock. */
|
|
9
|
+
export interface MockResponse {
|
|
10
|
+
status: number;
|
|
11
|
+
body?: string;
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
/** Artificial delay — e.g. `'100ms'`, `'1s'` */
|
|
14
|
+
delay?: string;
|
|
15
|
+
}
|
|
16
|
+
/** A full HTTP mock definition. */
|
|
17
|
+
export interface HttpMock {
|
|
18
|
+
id: string;
|
|
19
|
+
request: MockRequest;
|
|
20
|
+
response: MockResponse;
|
|
21
|
+
}
|
|
22
|
+
/** A single patch applied when a scenario is activated. */
|
|
23
|
+
export interface ScenarioPatch {
|
|
24
|
+
mock_id: string;
|
|
25
|
+
status?: number;
|
|
26
|
+
body?: string;
|
|
27
|
+
delay?: string;
|
|
28
|
+
}
|
|
29
|
+
/** A named scenario that patches one or more mock responses when activated. */
|
|
30
|
+
export interface Scenario {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
patches: ScenarioPatch[];
|
|
34
|
+
}
|
|
35
|
+
/** Global fault injection configuration. */
|
|
36
|
+
export interface FaultConfig {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
/** Artificial delay added to every request — e.g. `'200ms'` */
|
|
39
|
+
delay?: string;
|
|
40
|
+
/** Override the HTTP status of every matched response */
|
|
41
|
+
status_override?: number;
|
|
42
|
+
/** Probability (0–1) that the override fires; 0 means always */
|
|
43
|
+
error_rate?: number;
|
|
44
|
+
}
|
|
45
|
+
/** Options accepted by `MocklyServer.create()`. */
|
|
46
|
+
export interface MocklyServerOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Scenarios to include in the startup config.
|
|
49
|
+
* Scenarios can only be activated/deactivated via the management API;
|
|
50
|
+
* they cannot be created dynamically after the server starts.
|
|
51
|
+
*/
|
|
52
|
+
scenarios?: Scenario[];
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,mCAAmC;AACnC,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,WAAW,CAAA;IACpB,QAAQ,EAAE,YAAY,CAAA;CACvB;AAED,2DAA2D;AAC3D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,+EAA+E;AAC/E,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,aAAa,EAAE,CAAA;CACzB;AAED,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,yDAAyD;IACzD,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,mDAAmD;AACnD,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAA;CACvB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a free TCP port on 127.0.0.1.
|
|
3
|
+
* Always allocate ports sequentially, never in parallel, to avoid TOCTOU
|
|
4
|
+
* races where two concurrent calls could receive the same port.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getFreePort(): Promise<number>;
|
|
7
|
+
/**
|
|
8
|
+
* Returns n free TCP ports allocated atomically: all sockets are held open
|
|
9
|
+
* simultaneously so the OS cannot reuse them for each other, then released
|
|
10
|
+
* together. This avoids the race condition that arises from calling
|
|
11
|
+
* getFreePort() sequentially.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getFreePorts(n: number): Promise<number[]>;
|
|
14
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAQ7C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAuBzD;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a free TCP port on 127.0.0.1.
|
|
4
|
+
* Always allocate ports sequentially, never in parallel, to avoid TOCTOU
|
|
5
|
+
* races where two concurrent calls could receive the same port.
|
|
6
|
+
*/
|
|
7
|
+
export function getFreePort() {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const srv = net.createServer();
|
|
10
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
11
|
+
const port = srv.address().port;
|
|
12
|
+
srv.close(() => resolve(port));
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns n free TCP ports allocated atomically: all sockets are held open
|
|
18
|
+
* simultaneously so the OS cannot reuse them for each other, then released
|
|
19
|
+
* together. This avoids the race condition that arises from calling
|
|
20
|
+
* getFreePort() sequentially.
|
|
21
|
+
*/
|
|
22
|
+
export function getFreePorts(n) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const servers = [];
|
|
25
|
+
const ports = [];
|
|
26
|
+
let opened = 0;
|
|
27
|
+
for (let i = 0; i < n; i++) {
|
|
28
|
+
const srv = net.createServer();
|
|
29
|
+
servers.push(srv);
|
|
30
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
31
|
+
ports.push(srv.address().port);
|
|
32
|
+
if (++opened === n) {
|
|
33
|
+
let closed = 0;
|
|
34
|
+
for (const s of servers) {
|
|
35
|
+
s.close((err) => {
|
|
36
|
+
if (err) {
|
|
37
|
+
reject(err);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (++closed === n)
|
|
41
|
+
resolve(ports);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function sleep(ms) {
|
|
50
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,KAAK,CAAA;AAErB;;;;GAIG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,GAAG,CAAC,YAAY,EAAE,CAAA;QAC9B,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;YAC9B,MAAM,IAAI,GAAI,GAAG,CAAC,OAAO,EAAsB,CAAC,IAAI,CAAA;YACpD,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;QAChC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,CAAS;IACpC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,OAAO,GAAiB,EAAE,CAAA;QAChC,MAAM,KAAK,GAAa,EAAE,CAAA;QAC1B,IAAI,MAAM,GAAG,CAAC,CAAA;QAEd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,MAAM,GAAG,GAAG,GAAG,CAAC,YAAY,EAAE,CAAA;YAC9B,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACjB,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;gBAC9B,KAAK,CAAC,IAAI,CAAE,GAAG,CAAC,OAAO,EAAsB,CAAC,IAAI,CAAC,CAAA;gBACnD,IAAI,EAAE,MAAM,KAAK,CAAC,EAAE,CAAC;oBACnB,IAAI,MAAM,GAAG,CAAC,CAAA;oBACd,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;wBACxB,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;4BACd,IAAI,GAAG,EAAE,CAAC;gCAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gCAAC,OAAM;4BAAC,CAAC;4BAChC,IAAI,EAAE,MAAM,KAAK,CAAC;gCAAE,OAAO,CAAC,KAAK,CAAC,CAAA;wBACpC,CAAC,CAAC,CAAA;oBACJ,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;AAC9C,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dever-labs/mockly-driver",
|
|
3
|
+
"version": "v0.4.6",
|
|
4
|
+
"description": "Node.js client for Mockly — start/stop servers and manage HTTP mocks in tests",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mock",
|
|
7
|
+
"testing",
|
|
8
|
+
"http",
|
|
9
|
+
"integration-test",
|
|
10
|
+
"mockly",
|
|
11
|
+
"test-server",
|
|
12
|
+
"driver"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/dever-labs/mockly.git",
|
|
18
|
+
"directory": "clients/node"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/dever-labs/mockly/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/dever-labs/mockly/tree/main/clients/node#readme",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"bin": {
|
|
37
|
+
"mockly-install": "./bin/install.mjs"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"bin",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc --project tsconfig.json",
|
|
46
|
+
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"prepublishOnly": "npm run build"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"js-yaml": "^4.1.0"
|
|
52
|
+
},
|
|
53
|
+
"optionalDependencies": {
|
|
54
|
+
"@dever-labs/mockly-driver-linux-x64": "v0.4.6",
|
|
55
|
+
"@dever-labs/mockly-driver-linux-arm64": "v0.4.6",
|
|
56
|
+
"@dever-labs/mockly-driver-darwin-x64": "v0.4.6",
|
|
57
|
+
"@dever-labs/mockly-driver-darwin-arm64": "v0.4.6",
|
|
58
|
+
"@dever-labs/mockly-driver-win32-x64": "v0.4.6"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/js-yaml": "^4.0.9",
|
|
62
|
+
"@types/node": "^22.0.0",
|
|
63
|
+
"typescript": "^5.0.0",
|
|
64
|
+
"vitest": "^4.1.2"
|
|
65
|
+
}
|
|
66
|
+
}
|