@scenarist/msw-adapter 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +302 -27
- package/dist/conversion/response-builder.d.ts +3 -0
- package/dist/conversion/response-builder.d.ts.map +1 -0
- package/dist/conversion/response-builder.js +15 -0
- package/dist/handlers/dynamic-handler.d.ts +11 -0
- package/dist/handlers/dynamic-handler.d.ts.map +1 -0
- package/dist/handlers/dynamic-handler.js +103 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/matching/mock-matcher.d.ts +3 -0
- package/dist/matching/mock-matcher.d.ts.map +1 -0
- package/dist/matching/mock-matcher.js +8 -0
- package/dist/matching/url-matcher.d.ts +38 -0
- package/dist/matching/url-matcher.d.ts.map +1 -0
- package/dist/matching/url-matcher.js +142 -0
- package/package.json +66 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Paul Hammond (citypaul)
|
|
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
CHANGED
|
@@ -1,45 +1,320 @@
|
|
|
1
1
|
# @scenarist/msw-adapter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**⚠️ INTERNAL PACKAGE - DO NOT INSTALL DIRECTLY**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This package is an internal implementation detail of Scenarist and is **NOT published to npm**. It exists as a workspace dependency used by framework adapters.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Why is this package private?
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**You should install a framework adapter instead:**
|
|
10
|
+
- ✅ `@scenarist/express-adapter` - For Express applications
|
|
11
|
+
- ✅ `@scenarist/nextjs-adapter` - For Next.js applications
|
|
12
|
+
- ✅ `@scenarist/core` - For building custom adapters
|
|
10
13
|
|
|
11
|
-
This package
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@scenarist/msw-adapter`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
14
|
+
**This package is private because:**
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
1. **Not user-facing** - Users never import from `@scenarist/msw-adapter` directly
|
|
17
|
+
2. **Implementation detail** - Provides MSW integration layer used internally by framework adapters
|
|
18
|
+
3. **Prevents confusion** - Publishing would mislead users into thinking they should install it
|
|
19
|
+
4. **Version coupling** - Framework adapters control which msw-adapter version they use
|
|
20
|
+
5. **Simpler maintenance** - Internal packages don't need semver/breaking change coordination
|
|
21
|
+
6. **Cleaner API surface** - Framework adapters ARE the public API, msw-adapter is hidden complexity
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
**Dependency structure:**
|
|
24
|
+
```
|
|
25
|
+
Your App
|
|
26
|
+
├─ @scenarist/express-adapter (public, install this!)
|
|
27
|
+
│ └─ @scenarist/msw-adapter (private, workspace:*)
|
|
28
|
+
│ └─ @scenarist/core (public)
|
|
29
|
+
│
|
|
30
|
+
└─ @scenarist/nextjs-adapter (public, install this!)
|
|
31
|
+
└─ @scenarist/msw-adapter (private, workspace:*)
|
|
32
|
+
└─ @scenarist/core (public)
|
|
33
|
+
```
|
|
19
34
|
|
|
20
|
-
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## What is Scenarist?
|
|
21
38
|
|
|
22
|
-
|
|
39
|
+
**Scenarist** enables concurrent E2E tests to run with different backend states by switching mock scenarios at runtime via test IDs. This package provides the MSW integration layer that makes it all work.
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
2. Configure the trusted publisher (e.g., GitHub Actions)
|
|
26
|
-
3. Specify the repository and workflow that should be allowed to publish
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
41
|
+
**The big picture:**
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
```
|
|
44
|
+
Your App → Scenarist Adapter (Express/Next.js) → MSW Adapter → MSW → Intercepted HTTP
|
|
45
|
+
```
|
|
30
46
|
|
|
31
|
-
|
|
32
|
-
- Contains no executable code
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
47
|
+
**What this package does:**
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
Converts Scenarist's serializable `ScenaristMock` data into MSW `HttpHandler` instances at runtime, enabling Scenarist to work with any Node.js framework.
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
51
|
+
**Status:** ✅ Stable - Used internally by all Scenarist framework adapters
|
|
42
52
|
|
|
43
53
|
---
|
|
44
54
|
|
|
45
|
-
|
|
55
|
+
## For Framework Adapter Developers
|
|
56
|
+
|
|
57
|
+
If you're building a custom framework adapter for Scenarist, this package provides the MSW integration you need. Otherwise, you should use one of the existing public adapters listed above.
|
|
58
|
+
|
|
59
|
+
## What does this package provide?
|
|
60
|
+
|
|
61
|
+
The MSW adapter is the bridge between Scenarist's serializable mock definitions and MSW's HTTP interception. It provides:
|
|
62
|
+
|
|
63
|
+
1. **URL Matching** - Converts URL patterns (with wildcards and path params) into MSW-compatible matchers
|
|
64
|
+
2. **Mock Matching** - Finds the right mock definition for incoming requests (with request content matching)
|
|
65
|
+
3. **Response Building** - Transforms `ScenaristMock` into MSW `HttpResponse`
|
|
66
|
+
4. **Dynamic Handler** - Creates a single MSW handler that routes based on active scenarios
|
|
67
|
+
|
|
68
|
+
## Core Capabilities
|
|
69
|
+
|
|
70
|
+
This adapter implements all 25 Scenarist capabilities:
|
|
71
|
+
|
|
72
|
+
### Request Matching (6 capabilities)
|
|
73
|
+
- **Body matching** (partial match) - Match on request body fields
|
|
74
|
+
- **Header matching** (exact, case-insensitive) - Match on header values
|
|
75
|
+
- **Query matching** (exact) - Match on query parameters
|
|
76
|
+
- **Combined matching** - Combine body + headers + query
|
|
77
|
+
- **Specificity-based selection** - Most specific mock wins
|
|
78
|
+
- **Fallback mocks** - Mocks without criteria act as catch-all
|
|
79
|
+
|
|
80
|
+
### Response Sequences (4 capabilities)
|
|
81
|
+
- **Single responses** - Return same response every time
|
|
82
|
+
- **Response sequences** - Ordered responses for polling scenarios
|
|
83
|
+
- **Repeat modes** - `last`, `cycle`, `none` behaviors
|
|
84
|
+
- **Sequence exhaustion** - Skip exhausted sequences to fallback
|
|
85
|
+
|
|
86
|
+
### Stateful Mocks (6 capabilities)
|
|
87
|
+
- **State capture** - Extract values from requests
|
|
88
|
+
- **State injection** - Inject state into responses via templates
|
|
89
|
+
- **Array append** - Syntax: `stateKey[]` for arrays
|
|
90
|
+
- **Nested paths** - Dot notation: `user.profile.name`
|
|
91
|
+
- **State isolation** - Per test ID isolation
|
|
92
|
+
- **State reset** - Fresh state on scenario switch
|
|
93
|
+
|
|
94
|
+
### URL Patterns (4 capabilities)
|
|
95
|
+
- **Exact matches** - `https://api.example.com/users`
|
|
96
|
+
- **Wildcards** - `*/api/*`, `https://*/users`
|
|
97
|
+
- **Path parameters** - `/users/:id`, `/posts/:postId/comments/:commentId`
|
|
98
|
+
- **Native RegExp** - `/\/api\/v\d+\//`, `/\/users\/\d+$/` (weak comparison per MSW behavior)
|
|
99
|
+
|
|
100
|
+
### MSW Integration (6 capabilities)
|
|
101
|
+
- **Dynamic handler generation** - Single handler routes at runtime
|
|
102
|
+
- **Response building** - Status codes, JSON bodies, headers, delays
|
|
103
|
+
- **Automatic default fallback** - Collects default + active scenario mocks together
|
|
104
|
+
- **Strict mode** - Fail on unmocked requests
|
|
105
|
+
- **Test ID isolation** - Separate state per test ID
|
|
106
|
+
- **Framework-agnostic** - Works with any Node.js framework
|
|
107
|
+
|
|
108
|
+
## Features
|
|
109
|
+
|
|
110
|
+
- ✅ **URL pattern matching** - Exact matches, glob patterns (`*/api/*`), path parameters (`/users/:id`)
|
|
111
|
+
- ✅ **Request content matching** - Body, headers, query with specificity-based selection
|
|
112
|
+
- ✅ **Dynamic MSW handler generation** - Single handler routes to correct scenario at runtime
|
|
113
|
+
- ✅ **Response building** - Status codes, JSON bodies, headers, delays, state injection
|
|
114
|
+
- ✅ **Sequences and state** - Polling scenarios and stateful mocks fully supported
|
|
115
|
+
- ✅ **Automatic default fallback** - Collects default + active scenario mocks, uses specificity to select best match
|
|
116
|
+
- ✅ **Framework-agnostic** - Works with Express, Next.js, and any Node.js framework via HTTP-level interception
|
|
117
|
+
|
|
118
|
+
## Internal Package
|
|
119
|
+
|
|
120
|
+
⚠️ **This is an internal package used by framework adapters.**
|
|
121
|
+
|
|
122
|
+
You typically don't install or use this directly. Instead, use a framework-specific adapter:
|
|
123
|
+
|
|
124
|
+
- **[@scenarist/express-adapter](../../packages/express-adapter)** - For Express applications
|
|
125
|
+
- **[@scenarist/nextjs-adapter](../../packages/nextjs-adapter)** - For Next.js applications (App Router + Pages Router)
|
|
126
|
+
|
|
127
|
+
## How It Works
|
|
128
|
+
|
|
129
|
+
### 1. URL Matching
|
|
130
|
+
|
|
131
|
+
Converts string patterns into MSW-compatible URL matchers:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { matchesUrl } from '@scenarist/msw-adapter';
|
|
135
|
+
|
|
136
|
+
matchesUrl('https://api.github.com/users/octocat', 'GET', 'https://api.github.com/users/:username');
|
|
137
|
+
// Returns: { matches: true, params: { username: 'octocat' } }
|
|
138
|
+
|
|
139
|
+
matchesUrl('https://api.stripe.com/v1/charges', 'POST', '*/v1/charges');
|
|
140
|
+
// Returns: { matches: true, params: {} }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Supported patterns (three types with different hostname matching):**
|
|
144
|
+
|
|
145
|
+
**1. Pathname-only patterns** (origin-agnostic - match ANY hostname)
|
|
146
|
+
- Path params: `/users/:id`, `/posts/:postId/comments/:commentId`
|
|
147
|
+
- Exact: `/api/users`
|
|
148
|
+
- Wildcards: `/api/*`, `*/users/*`
|
|
149
|
+
|
|
150
|
+
**2. Full URL patterns** (hostname-specific - match ONLY specified hostname)
|
|
151
|
+
- Exact: `https://api.example.com/users`
|
|
152
|
+
- Path params: `https://api.example.com/users/:id`
|
|
153
|
+
- Wildcards: `https://*/users`, `https://api.example.com/*`
|
|
154
|
+
|
|
155
|
+
**3. Native RegExp** (origin-agnostic - MSW weak comparison)
|
|
156
|
+
- `/\/api\/v\d+\//` (matches /api/v1/, /api/v2/, etc.)
|
|
157
|
+
- `/\/users\/\d+$/` (matches /users/123 at any origin)
|
|
158
|
+
|
|
159
|
+
**IMPORTANT:** If you specify a hostname in a full URL pattern, it WILL be matched. Choose pathname patterns for environment-agnostic mocks, full URL patterns when hostname matters.
|
|
160
|
+
|
|
161
|
+
### 2. Mock Matching
|
|
162
|
+
|
|
163
|
+
Finds the right mock for a request:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { findMatchingMock } from '@scenarist/msw-adapter';
|
|
167
|
+
|
|
168
|
+
const mocks: ScenaristMock[] = [
|
|
169
|
+
{ method: 'GET', url: '/users/:id', response: { status: 200, body: {...} } },
|
|
170
|
+
{ method: 'POST', url: '/users', response: { status: 201, body: {...} } },
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const mock = findMatchingMock(mocks, 'GET', 'https://api.example.com/users/123');
|
|
174
|
+
// Returns the first mock (matches /users/:id)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 3. Response Building
|
|
178
|
+
|
|
179
|
+
Converts `ScenaristMock` to MSW `HttpResponse`:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { buildResponse } from '@scenarist/msw-adapter';
|
|
183
|
+
|
|
184
|
+
const mockDef: ScenaristMock = {
|
|
185
|
+
method: 'GET',
|
|
186
|
+
url: '/api/user',
|
|
187
|
+
response: {
|
|
188
|
+
status: 200,
|
|
189
|
+
body: { id: '123', name: 'John' },
|
|
190
|
+
headers: { 'X-Custom': 'value' },
|
|
191
|
+
delay: 100,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const response = await buildResponse(mockDef);
|
|
196
|
+
// Returns MSW HttpResponse with status, body, headers, and delay
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 4. Dynamic Handler
|
|
200
|
+
|
|
201
|
+
The core integration - a single MSW handler that routes dynamically:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import { createDynamicHandler } from '@scenarist/msw-adapter';
|
|
205
|
+
import { setupServer } from 'msw/node';
|
|
206
|
+
|
|
207
|
+
const handler = createDynamicHandler({
|
|
208
|
+
getTestId: () => testIdStorage.getStore() ?? 'default-test',
|
|
209
|
+
getActiveScenario: (testId) => manager.getActiveScenario(testId),
|
|
210
|
+
getScenarioDefinition: (scenarioId) => manager.getScenarioById(scenarioId),
|
|
211
|
+
strictMode: false,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const server = setupServer(handler);
|
|
215
|
+
server.listen();
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**How it works:**
|
|
219
|
+
1. Gets test ID from AsyncLocalStorage (or other context)
|
|
220
|
+
2. Looks up active scenario for that test ID
|
|
221
|
+
3. **Collects mocks from BOTH default AND active scenario** for matching URL + method
|
|
222
|
+
4. Uses specificity-based selection to choose best match
|
|
223
|
+
5. Active scenario mocks (with match criteria) override default mocks (no criteria)
|
|
224
|
+
6. Returns mocked response or passthrough (if no match found)
|
|
225
|
+
|
|
226
|
+
**Key insight:** Default scenario mocks are ALWAYS collected first, then active scenario mocks are added. The specificity algorithm (from `ResponseSelector`) chooses the most specific match. This means specialized scenarios only need to define what changes - everything else automatically falls back to default.
|
|
227
|
+
|
|
228
|
+
## Architecture
|
|
229
|
+
|
|
230
|
+
This package is designed to be framework-agnostic. Framework adapters (Express, Next.js, etc.) handle:
|
|
231
|
+
|
|
232
|
+
- Test ID extraction (from headers, context, etc.)
|
|
233
|
+
- Scenario management (switching, retrieving)
|
|
234
|
+
- Middleware integration
|
|
235
|
+
|
|
236
|
+
The MSW adapter handles:
|
|
237
|
+
|
|
238
|
+
- URL matching and pattern conversion
|
|
239
|
+
- Mock finding and selection
|
|
240
|
+
- Response building
|
|
241
|
+
- MSW handler creation
|
|
242
|
+
|
|
243
|
+
## API Reference
|
|
244
|
+
|
|
245
|
+
### URL Matching
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
export const matchesUrl = (
|
|
249
|
+
requestUrl: string,
|
|
250
|
+
method: string,
|
|
251
|
+
pattern: string
|
|
252
|
+
): { matches: boolean; params: Record<string, string> };
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Mock Matching
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
export const findMatchingMock = (
|
|
259
|
+
mocks: ReadonlyArray<ScenaristMock>,
|
|
260
|
+
method: string,
|
|
261
|
+
url: string
|
|
262
|
+
): ScenaristMock | undefined;
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Response Building
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
export const buildResponse = (
|
|
269
|
+
mockDef: ScenaristMock
|
|
270
|
+
): Promise<HttpResponse>;
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Dynamic Handler
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
export type DynamicHandlerOptions = {
|
|
277
|
+
readonly getTestId: () => string;
|
|
278
|
+
readonly getActiveScenario: (testId: string) => ActiveScenario | undefined;
|
|
279
|
+
readonly getScenarioDefinition: (scenarioId: string) => ScenaristScenario | undefined;
|
|
280
|
+
readonly strictMode: boolean;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
export const createDynamicHandler = (
|
|
284
|
+
options: DynamicHandlerOptions
|
|
285
|
+
): HttpHandler;
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## TypeScript
|
|
289
|
+
|
|
290
|
+
Full TypeScript support with strict mode enabled.
|
|
291
|
+
|
|
292
|
+
**Exported types:**
|
|
293
|
+
```typescript
|
|
294
|
+
import type {
|
|
295
|
+
DynamicHandlerOptions,
|
|
296
|
+
} from '@scenarist/msw-adapter';
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Testing
|
|
300
|
+
|
|
301
|
+
This package has comprehensive test coverage:
|
|
302
|
+
|
|
303
|
+
- ✅ URL matching (exact, glob, path params)
|
|
304
|
+
- ✅ Mock matching (method, URL, precedence)
|
|
305
|
+
- ✅ Response building (status, body, headers, delays)
|
|
306
|
+
- ✅ Dynamic handler (scenarios, fallback, strict mode)
|
|
307
|
+
- ✅ **31 tests passing**
|
|
308
|
+
|
|
309
|
+
## Contributing
|
|
310
|
+
|
|
311
|
+
See [CONTRIBUTING.md](../../CONTRIBUTING.md) for development guidelines.
|
|
312
|
+
|
|
313
|
+
## License
|
|
314
|
+
|
|
315
|
+
MIT
|
|
316
|
+
|
|
317
|
+
## Related Packages
|
|
318
|
+
|
|
319
|
+
- **[@scenarist/core](../core)** - Core scenario management
|
|
320
|
+
- **[@scenarist/express-adapter](../express-adapter)** - Express integration (uses this package)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-builder.d.ts","sourceRoot":"","sources":["../../src/conversion/response-builder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,eAAO,MAAM,aAAa,GACxB,UAAU,iBAAiB,KAC1B,OAAO,CAAC,QAAQ,CAclB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HttpResponse, delay } from 'msw';
|
|
2
|
+
export const buildResponse = async (response) => {
|
|
3
|
+
if (response.delay) {
|
|
4
|
+
await delay(response.delay);
|
|
5
|
+
}
|
|
6
|
+
// Type assertion required: MSW's HttpResponse.json() expects JsonBodyType,
|
|
7
|
+
// but ScenaristResponse.body is typed as 'unknown' to maintain
|
|
8
|
+
// framework-agnostic serialization in core package. The body is guaranteed
|
|
9
|
+
// to be JSON-serializable by ScenaristResponse design contract. We do not
|
|
10
|
+
// perform runtime validation for performance reasons (garbage in, garbage out).
|
|
11
|
+
return HttpResponse.json(response.body, {
|
|
12
|
+
status: response.status,
|
|
13
|
+
headers: response.headers,
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HttpHandler } from 'msw';
|
|
2
|
+
import type { ActiveScenario, ScenaristScenario, ResponseSelector } from '@scenarist/core';
|
|
3
|
+
export type DynamicHandlerOptions = {
|
|
4
|
+
readonly getTestId: (request: Request) => string;
|
|
5
|
+
readonly getActiveScenario: (testId: string) => ActiveScenario | undefined;
|
|
6
|
+
readonly getScenarioDefinition: (scenarioId: string) => ScenaristScenario | undefined;
|
|
7
|
+
readonly strictMode: boolean;
|
|
8
|
+
readonly responseSelector: ResponseSelector;
|
|
9
|
+
};
|
|
10
|
+
export declare const createDynamicHandler: (options: DynamicHandlerOptions) => HttpHandler;
|
|
11
|
+
//# sourceMappingURL=dynamic-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/dynamic-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EAIjB,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AAIzB,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACjD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,GAAG,SAAS,CAAC;IAC3E,QAAQ,CAAC,qBAAqB,EAAE,CAC9B,UAAU,EAAE,MAAM,KACf,iBAAiB,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;CAC7C,CAAC;AAgGF,eAAO,MAAM,oBAAoB,GAC/B,SAAS,qBAAqB,KAC7B,WA8BF,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { http, passthrough } from 'msw';
|
|
2
|
+
import { buildResponse } from '../conversion/response-builder.js';
|
|
3
|
+
import { matchesUrl } from '../matching/url-matcher.js';
|
|
4
|
+
/**
|
|
5
|
+
* Extract HttpRequestContext from MSW Request object.
|
|
6
|
+
* Converts MSW request to the format expected by ResponseSelector.
|
|
7
|
+
*/
|
|
8
|
+
const extractHttpRequestContext = async (request) => {
|
|
9
|
+
// Parse request body if present
|
|
10
|
+
let body = undefined;
|
|
11
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
12
|
+
try {
|
|
13
|
+
const clonedRequest = request.clone();
|
|
14
|
+
body = await clonedRequest.json();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Body is not JSON or doesn't exist
|
|
18
|
+
body = undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Extract headers as Record<string, string>
|
|
22
|
+
// Headers are passed through as-is; normalization is core's responsibility.
|
|
23
|
+
// The Fetch API Headers object already normalizes keys to lowercase,
|
|
24
|
+
// but even if it didn't, core would handle normalization.
|
|
25
|
+
const headers = {};
|
|
26
|
+
request.headers.forEach((value, key) => {
|
|
27
|
+
headers[key] = value;
|
|
28
|
+
});
|
|
29
|
+
// Extract query parameters from URL
|
|
30
|
+
const url = new URL(request.url);
|
|
31
|
+
const query = {};
|
|
32
|
+
url.searchParams.forEach((value, key) => {
|
|
33
|
+
query[key] = value;
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
method: request.method,
|
|
37
|
+
url: request.url,
|
|
38
|
+
body,
|
|
39
|
+
headers,
|
|
40
|
+
query,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Get mocks from active scenario, with default scenario mocks as fallback.
|
|
45
|
+
* Returns URL-matching mocks with their extracted params for ResponseSelector to evaluate.
|
|
46
|
+
*
|
|
47
|
+
* Default mocks are ALWAYS included (if they match URL+method).
|
|
48
|
+
* Active scenario mocks are added after defaults, allowing them to override
|
|
49
|
+
* based on specificity (mocks with match criteria have higher specificity).
|
|
50
|
+
*
|
|
51
|
+
* Each mock is paired with params extracted from its URL pattern.
|
|
52
|
+
* After ResponseSelector chooses a mock, we use THAT mock's params.
|
|
53
|
+
*/
|
|
54
|
+
const getMocksFromScenarios = (activeScenario, getScenarioDefinition, method, url) => {
|
|
55
|
+
const mocksWithParams = [];
|
|
56
|
+
// Step 1: ALWAYS include default scenario mocks first
|
|
57
|
+
// These act as fallback when active scenario mocks don't match
|
|
58
|
+
const defaultScenario = getScenarioDefinition('default');
|
|
59
|
+
if (defaultScenario) {
|
|
60
|
+
defaultScenario.mocks.forEach((mock) => {
|
|
61
|
+
const methodMatches = mock.method.toUpperCase() === method.toUpperCase();
|
|
62
|
+
const urlMatch = matchesUrl(mock.url, url);
|
|
63
|
+
if (methodMatches && urlMatch.matches) {
|
|
64
|
+
mocksWithParams.push({ mock, params: urlMatch.params });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// Step 2: Add active scenario mocks (if any)
|
|
69
|
+
// These override defaults based on specificity (via ResponseSelector)
|
|
70
|
+
if (activeScenario) {
|
|
71
|
+
const scenarioDefinition = getScenarioDefinition(activeScenario.scenarioId);
|
|
72
|
+
if (scenarioDefinition) {
|
|
73
|
+
scenarioDefinition.mocks.forEach((mock) => {
|
|
74
|
+
const methodMatches = mock.method.toUpperCase() === method.toUpperCase();
|
|
75
|
+
const urlMatch = matchesUrl(mock.url, url);
|
|
76
|
+
if (methodMatches && urlMatch.matches) {
|
|
77
|
+
mocksWithParams.push({ mock, params: urlMatch.params });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return mocksWithParams;
|
|
83
|
+
};
|
|
84
|
+
export const createDynamicHandler = (options) => {
|
|
85
|
+
return http.all('*', async ({ request }) => {
|
|
86
|
+
const testId = options.getTestId(request);
|
|
87
|
+
const activeScenario = options.getActiveScenario(testId);
|
|
88
|
+
const scenarioId = activeScenario?.scenarioId ?? 'default';
|
|
89
|
+
// Extract request context for matching
|
|
90
|
+
const context = await extractHttpRequestContext(request);
|
|
91
|
+
// Get candidate mocks from active or default scenario
|
|
92
|
+
const mocks = getMocksFromScenarios(activeScenario, options.getScenarioDefinition, request.method, request.url);
|
|
93
|
+
// Use injected ResponseSelector to find matching mock
|
|
94
|
+
const result = options.responseSelector.selectResponse(testId, scenarioId, context, mocks);
|
|
95
|
+
if (result.success) {
|
|
96
|
+
return buildResponse(result.data);
|
|
97
|
+
}
|
|
98
|
+
if (options.strictMode) {
|
|
99
|
+
return new Response(null, { status: 501 });
|
|
100
|
+
}
|
|
101
|
+
return passthrough();
|
|
102
|
+
});
|
|
103
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { matchesUrl } from './matching/url-matcher.js';
|
|
2
|
+
export type { UrlMatchResult } from './matching/url-matcher.js';
|
|
3
|
+
export { findMatchingMock } from './matching/mock-matcher.js';
|
|
4
|
+
export { buildResponse } from './conversion/response-builder.js';
|
|
5
|
+
export { createDynamicHandler } from './handlers/dynamic-handler.js';
|
|
6
|
+
export type { DynamicHandlerOptions } from './handlers/dynamic-handler.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAE9D,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Public API
|
|
2
|
+
// These exports are primarily intended for use by Scenarist adapters
|
|
3
|
+
// (e.g., express-adapter, fastify-adapter). End users should use the
|
|
4
|
+
// higher-level adapter packages rather than these low-level utilities directly.
|
|
5
|
+
export { matchesUrl } from './matching/url-matcher.js';
|
|
6
|
+
export { findMatchingMock } from './matching/mock-matcher.js';
|
|
7
|
+
export { buildResponse } from './conversion/response-builder.js';
|
|
8
|
+
export { createDynamicHandler } from './handlers/dynamic-handler.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-matcher.d.ts","sourceRoot":"","sources":["../../src/matching/mock-matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,eAAO,MAAM,gBAAgB,GAC3B,OAAO,aAAa,CAAC,aAAa,CAAC,EACnC,QAAQ,MAAM,EACd,KAAK,MAAM,KACV,aAAa,GAAG,SAMlB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { matchesUrl } from './url-matcher.js';
|
|
2
|
+
export const findMatchingMock = (mocks, method, url) => {
|
|
3
|
+
return mocks.find((mock) => {
|
|
4
|
+
const methodMatches = mock.method.toUpperCase() === method.toUpperCase();
|
|
5
|
+
const urlMatch = matchesUrl(mock.url, url);
|
|
6
|
+
return methodMatches && urlMatch.matches;
|
|
7
|
+
});
|
|
8
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result of URL matching with path parameter extraction.
|
|
3
|
+
*
|
|
4
|
+
* Matches MSW's documented param types:
|
|
5
|
+
* - string for simple params (:id)
|
|
6
|
+
* - string[] for repeating params (:path+)
|
|
7
|
+
*/
|
|
8
|
+
export type UrlMatchResult = {
|
|
9
|
+
readonly matches: boolean;
|
|
10
|
+
readonly params?: Readonly<Record<string, string | ReadonlyArray<string>>>;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Match URL patterns using MSW-compatible matching logic.
|
|
14
|
+
*
|
|
15
|
+
* Delegates to path-to-regexp v6 (same as MSW 2.x) for all string patterns.
|
|
16
|
+
* This ensures automatic MSW parity for path parameter extraction.
|
|
17
|
+
*
|
|
18
|
+
* Matching strategies:
|
|
19
|
+
* 1. RegExp patterns: MSW weak comparison (substring matching, origin-agnostic)
|
|
20
|
+
* 2. Pathname-only patterns: Origin-agnostic (match any hostname)
|
|
21
|
+
* 3. Full URL patterns: Hostname-specific (must match exactly)
|
|
22
|
+
*
|
|
23
|
+
* Pattern type examples:
|
|
24
|
+
* - Pathname: '/users/:id' matches 'http://localhost/users/123' AND 'https://api.com/users/123'
|
|
25
|
+
* - Full URL: 'http://api.com/users/:id' matches 'http://api.com/users/123' ONLY
|
|
26
|
+
*
|
|
27
|
+
* Path parameter examples (all handled by path-to-regexp):
|
|
28
|
+
* - Exact: '/users/123' matches '/users/123'
|
|
29
|
+
* - Simple params: '/users/:id' matches '/users/123' → {id: '123'}
|
|
30
|
+
* - Multiple params: '/users/:userId/posts/:postId' → {userId: 'alice', postId: '42'}
|
|
31
|
+
* - Optional params: '/files/:name?' matches '/files' or '/files/doc.txt'
|
|
32
|
+
* - Repeating params: '/files/:path+' matches '/files/a/b/c' → {path: ['a','b','c']}
|
|
33
|
+
* - Custom regex: '/orders/:id(\\d+)' matches '/orders/123' but not '/orders/abc'
|
|
34
|
+
*
|
|
35
|
+
* @see ADR-0016 for MSW weak comparison semantics
|
|
36
|
+
*/
|
|
37
|
+
export declare const matchesUrl: (pattern: string | RegExp, requestUrl: string) => UrlMatchResult;
|
|
38
|
+
//# sourceMappingURL=url-matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url-matcher.d.ts","sourceRoot":"","sources":["../../src/matching/url-matcher.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;CAC5E,CAAC;AA8DF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,UAAU,GACrB,SAAS,MAAM,GAAG,MAAM,EACxB,YAAY,MAAM,KACjB,cAuEF,CAAC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { match } from 'path-to-regexp';
|
|
2
|
+
/**
|
|
3
|
+
* Extract pathname from URL string, or return as-is if not a valid URL.
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: Handles path-to-regexp syntax (`:param`, `?`, `+`, `(regex)`)
|
|
6
|
+
* The URL constructor treats `?` as query string delimiter, which breaks optional params.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* - 'http://localhost:3001/api/files/:filename?' → '/api/files/:filename?' (preserves ?)
|
|
10
|
+
* - 'http://localhost:3001/api/files/:path+' → '/api/files/:path+' (preserves +)
|
|
11
|
+
* - '/api/users/:id' → '/api/users/:id' (already pathname)
|
|
12
|
+
*/
|
|
13
|
+
const extractPathnameOrReturnAsIs = (url) => {
|
|
14
|
+
// Match protocol://host pattern to manually extract pathname
|
|
15
|
+
// This preserves path-to-regexp syntax that URL constructor would corrupt
|
|
16
|
+
const urlPattern = /^https?:\/\/[^/]+(\/.*)?$/;
|
|
17
|
+
const match = urlPattern.exec(url);
|
|
18
|
+
if (match) {
|
|
19
|
+
// Return everything after the host (group 1), or '/' if no path
|
|
20
|
+
return match[1] || '/';
|
|
21
|
+
}
|
|
22
|
+
// Not a full URL, return as-is (already a pathname)
|
|
23
|
+
return url;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Extract params from path-to-regexp match result, filtering unnamed groups.
|
|
27
|
+
*
|
|
28
|
+
* MSW behavior (via path-to-regexp v6):
|
|
29
|
+
* - Named params (:id, :name) are included
|
|
30
|
+
* - Array params (:path+, :path*) are included as arrays
|
|
31
|
+
* - Unnamed groups like (user|u) create numeric keys ('0', '1', etc.) which are FILTERED OUT
|
|
32
|
+
*
|
|
33
|
+
* Returns Record<string, string | string[]> matching MSW's documented types.
|
|
34
|
+
*/
|
|
35
|
+
const extractParams = (params) => {
|
|
36
|
+
return Object.fromEntries(Object.entries(params).filter(([key, value]) => {
|
|
37
|
+
// Filter out unnamed groups (numeric keys like '0', '1', '2', etc.)
|
|
38
|
+
if (/^\d+$/.test(key)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
// Keep strings and arrays (MSW documented: string | string[])
|
|
42
|
+
return typeof value === 'string' || Array.isArray(value);
|
|
43
|
+
}));
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Extract hostname from URL, or return undefined if not a full URL.
|
|
47
|
+
*/
|
|
48
|
+
const extractHostname = (url) => {
|
|
49
|
+
const urlPattern = /^https?:\/\/([^/]+)/;
|
|
50
|
+
const match = urlPattern.exec(url);
|
|
51
|
+
return match ? match[1] : undefined;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Match URL patterns using MSW-compatible matching logic.
|
|
55
|
+
*
|
|
56
|
+
* Delegates to path-to-regexp v6 (same as MSW 2.x) for all string patterns.
|
|
57
|
+
* This ensures automatic MSW parity for path parameter extraction.
|
|
58
|
+
*
|
|
59
|
+
* Matching strategies:
|
|
60
|
+
* 1. RegExp patterns: MSW weak comparison (substring matching, origin-agnostic)
|
|
61
|
+
* 2. Pathname-only patterns: Origin-agnostic (match any hostname)
|
|
62
|
+
* 3. Full URL patterns: Hostname-specific (must match exactly)
|
|
63
|
+
*
|
|
64
|
+
* Pattern type examples:
|
|
65
|
+
* - Pathname: '/users/:id' matches 'http://localhost/users/123' AND 'https://api.com/users/123'
|
|
66
|
+
* - Full URL: 'http://api.com/users/:id' matches 'http://api.com/users/123' ONLY
|
|
67
|
+
*
|
|
68
|
+
* Path parameter examples (all handled by path-to-regexp):
|
|
69
|
+
* - Exact: '/users/123' matches '/users/123'
|
|
70
|
+
* - Simple params: '/users/:id' matches '/users/123' → {id: '123'}
|
|
71
|
+
* - Multiple params: '/users/:userId/posts/:postId' → {userId: 'alice', postId: '42'}
|
|
72
|
+
* - Optional params: '/files/:name?' matches '/files' or '/files/doc.txt'
|
|
73
|
+
* - Repeating params: '/files/:path+' matches '/files/a/b/c' → {path: ['a','b','c']}
|
|
74
|
+
* - Custom regex: '/orders/:id(\\d+)' matches '/orders/123' but not '/orders/abc'
|
|
75
|
+
*
|
|
76
|
+
* @see ADR-0016 for MSW weak comparison semantics
|
|
77
|
+
*/
|
|
78
|
+
export const matchesUrl = (pattern, requestUrl) => {
|
|
79
|
+
/**
|
|
80
|
+
* RegExp patterns: MSW Weak Comparison (ADR-0016)
|
|
81
|
+
*
|
|
82
|
+
* RegExp.test() performs substring matching (origin-agnostic).
|
|
83
|
+
* This matches MSW's documented behavior for regular expressions.
|
|
84
|
+
*
|
|
85
|
+
* Example: /\/posts\// matches:
|
|
86
|
+
* - 'http://localhost:8080/posts/' ✅
|
|
87
|
+
* - 'https://backend.dev/user/posts/' ✅
|
|
88
|
+
* - 'https://api.example.com/posts/123' ✅
|
|
89
|
+
*/
|
|
90
|
+
if (pattern instanceof RegExp) {
|
|
91
|
+
return { matches: pattern.test(requestUrl) };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* String patterns: Hostname-aware matching
|
|
95
|
+
*
|
|
96
|
+
* If pattern is a full URL (has hostname), hostname must match.
|
|
97
|
+
* If pattern is pathname-only, any hostname matches (origin-agnostic).
|
|
98
|
+
*
|
|
99
|
+
* This gives users choice:
|
|
100
|
+
* - Use pathname patterns for environment-agnostic mocks
|
|
101
|
+
* - Use full URL patterns when hostname matters
|
|
102
|
+
*/
|
|
103
|
+
const patternHostname = extractHostname(pattern);
|
|
104
|
+
const requestHostname = extractHostname(requestUrl);
|
|
105
|
+
// If pattern has hostname, it must match request hostname
|
|
106
|
+
if (patternHostname !== undefined && requestHostname !== undefined) {
|
|
107
|
+
if (patternHostname !== requestHostname) {
|
|
108
|
+
return { matches: false };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Pathname matching using path-to-regexp v6 (same as MSW 2.x)
|
|
113
|
+
*
|
|
114
|
+
* path-to-regexp handles:
|
|
115
|
+
* - Exact string matches
|
|
116
|
+
* - Path parameters (:id, :name, etc.)
|
|
117
|
+
* - Optional parameters (:id?)
|
|
118
|
+
* - Repeating parameters (:path+, :path*)
|
|
119
|
+
* - Custom regex parameters (:id(\\d+))
|
|
120
|
+
* - Unnamed groups filtering
|
|
121
|
+
*
|
|
122
|
+
* By using the same library as MSW, we automatically get MSW-compatible behavior.
|
|
123
|
+
*
|
|
124
|
+
* CRITICAL: Strip query parameters from request URL before matching.
|
|
125
|
+
* path-to-regexp matches against pathname only, not query string.
|
|
126
|
+
* Example: '/users/123?role=admin' should match pattern '/users/:id'
|
|
127
|
+
*/
|
|
128
|
+
const patternPath = extractPathnameOrReturnAsIs(pattern);
|
|
129
|
+
let requestPath = extractPathnameOrReturnAsIs(requestUrl);
|
|
130
|
+
// Strip query parameters from request path
|
|
131
|
+
const queryIndex = requestPath.indexOf('?');
|
|
132
|
+
if (queryIndex !== -1) {
|
|
133
|
+
requestPath = requestPath.substring(0, queryIndex);
|
|
134
|
+
}
|
|
135
|
+
const matcher = match(patternPath, { decode: decodeURIComponent });
|
|
136
|
+
const result = matcher(requestPath);
|
|
137
|
+
if (result) {
|
|
138
|
+
const params = extractParams(result.params);
|
|
139
|
+
return { matches: true, params };
|
|
140
|
+
}
|
|
141
|
+
return { matches: false };
|
|
142
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scenarist/msw-adapter",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Internal: MSW integration layer for Scenarist framework adapters",
|
|
5
|
+
"author": "Paul Hammond (citypaul) <paul@packsoftware.co.uk>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/citypaul/scenarist#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/citypaul/scenarist.git",
|
|
11
|
+
"directory": "internal/msw-adapter"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/citypaul/scenarist/issues"
|
|
15
|
+
},
|
|
5
16
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
"msw",
|
|
18
|
+
"mock-service-worker",
|
|
19
|
+
"testing",
|
|
20
|
+
"scenarist",
|
|
21
|
+
"internal"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"path-to-regexp": "^6.3.0",
|
|
45
|
+
"@scenarist/core": "0.1.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"msw": "^2.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^24.10.1",
|
|
52
|
+
"@vitest/coverage-v8": "^4.0.8",
|
|
53
|
+
"eslint": "^9.39.1",
|
|
54
|
+
"msw": "^2.12.1",
|
|
55
|
+
"typescript": "^5.9.3",
|
|
56
|
+
"vitest": "^4.0.8",
|
|
57
|
+
"@scenarist/eslint-config": "^0.0.0",
|
|
58
|
+
"@scenarist/typescript-config": "0.0.0"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsc",
|
|
62
|
+
"dev": "tsc --watch",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest",
|
|
65
|
+
"typecheck": "tsc --project tsconfig.typecheck.json --noEmit",
|
|
66
|
+
"lint": "eslint .",
|
|
67
|
+
"clean": "rm -rf dist"
|
|
68
|
+
}
|
|
69
|
+
}
|