@hellaweb3/wiremock-testcontainers-nodejs 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,8 +25,10 @@
25
25
  a [WireMock](https://wiremock.org/)
26
26
  instance with [Testcontainers](https://node.testcontainers.org/). It provides a
27
27
  fluent API to configure and start WireMock
28
- containers, including support for loading mappings and files from recorded
29
- Wiremock sessions.
28
+ containers from recorded WireMock directories, individual named mappings
29
+ (local files, npm packages, module-relative files or inline JSON) and static
30
+ files – an API modelled on the official
31
+ [Java module](https://github.com/wiremock/wiremock-testcontainers-java).
30
32
 
31
33
  ## Installation
32
34
 
@@ -34,35 +36,79 @@ Wiremock sessions.
34
36
  pnpm add -D @hellaweb3/wiremock-testcontainers-nodejs testcontainers
35
37
  ```
36
38
 
39
+ Requires Node.js >= 22.22 and a reachable Docker daemon (anything
40
+ [Testcontainers](https://node.testcontainers.org/supported-container-runtimes/)
41
+ supports).
42
+
43
+ **What you get:** a ~17 kB package – dual ESM (`dist/index.js`) and CJS
44
+ (`dist/index.cjs`) builds with matching declarations, `sideEffects: false`
45
+ for tree-shaking, `testcontainers` as the single runtime dependency (never
46
+ bundled), and the TypeScript source for go-to-definition.
47
+
48
+ **pnpm users:** pnpm >= 10 may report `Ignored build scripts: ssh2, protobufjs,
49
+ cpu-features`. These are transitive dependencies of `testcontainers`; the
50
+ scripts only build an *optional* native crypto binding for `ssh2`, which falls
51
+ back to pure JavaScript. It is safe to leave them ignored.
52
+
37
53
  ## Usage
38
54
 
39
55
  ### Basic Example
40
56
 
41
- Assuming you placed your mappings in `test/__mocks__/wiremock`,
57
+ 1. Record or hand-write your stubs in the standard WireMock layout, e.g.
58
+ `test/__mocks__/wiremock/mappings/*.json` and
59
+ `test/__mocks__/wiremock/__files/*.json`.
60
+ 2. Start a `WiremockContainer` once per suite, point it at that directory and
61
+ use `rpcUrl` as the base URL of the API under test.
62
+ 3. Stop the container when the suite finishes.
42
63
 
43
64
  ```typescript
44
- import { WiremockContainer } from "@hellaweb3/wiremock-testcontainers-nodejs";
65
+ import { afterAll, beforeAll, describe, expect, it } from "vitest"; // or jest
66
+ import {
67
+ StartedWiremockContainer,
68
+ WiremockContainer,
69
+ } from "@hellaweb3/wiremock-testcontainers-nodejs";
45
70
 
46
- describe("E2E Test", () => {
47
- let container;
71
+ describe("Cards API", () => {
72
+ let container: StartedWiremockContainer;
48
73
 
49
74
  beforeAll(async () => {
50
- container = await new WiremockContainer()
51
- .withMappings("./test/__mocks__/wiremock")
52
- .start();
53
- }, 60000);
75
+ container = await new WiremockContainer() // wiremock/wiremock:latest
76
+ .withMappings("./test/__mocks__/wiremock") // mappings/ + __files/
77
+ .start(); // waits for port 8080
78
+ }, 60_000); // image pull can be slow
54
79
 
55
80
  afterAll(async () => {
56
81
  await container.stop();
57
82
  });
58
83
 
59
- it("should interact with WireMock", async () => {
60
- const url = container.rpcUrl;
61
- // Your test logic here
84
+ it("serves the recorded GET /cards stub", async () => {
85
+ const response = await fetch(`${container.rpcUrl}/cards`);
86
+ expect(response.status).toBe(200);
87
+
88
+ const body = await response.json();
89
+ expect(body.items).toHaveLength(6);
90
+ });
91
+
92
+ it("exposes the WireMock admin API", async () => {
93
+ const response = await fetch(`${container.rpcUrl}/__admin/mappings`);
94
+ const { meta } = await response.json();
95
+ expect(meta.total).toBe(2);
62
96
  });
63
97
  });
64
98
  ```
65
99
 
100
+ `WiremockContainer` extends Testcontainers' `GenericContainer`, so every
101
+ builder method is available – for example to pin an image, name the container
102
+ or pass WireMock CLI flags:
103
+
104
+ ```typescript
105
+ const container = await new WiremockContainer("wiremock/wiremock:3.13.0")
106
+ .withName("wiremock-cards-api")
107
+ .withCommand(["--verbose", "--global-response-templating"])
108
+ .withMappings("./test/__mocks__/wiremock")
109
+ .start();
110
+ ```
111
+
66
112
  ### With Mappings and Files
67
113
 
68
114
  WireMock uses a specific directory structure for mappings and static files. This
@@ -88,21 +134,139 @@ const container = await new WiremockContainer()
88
134
  .start();
89
135
  ```
90
136
 
137
+ ### Individual mappings and files
138
+
139
+ `withMapping(name, source)` registers one stub at `mappings/<name>.json` and
140
+ `withFiles(...files)` registers response bodies in `__files/` – no recorded
141
+ directory required. A mapping file holds one WireMock stub, or several under
142
+ `"mappings"`:
143
+
144
+ ```json
145
+ { "request": { "method": "GET", "url": "/health" }, "response": { "status": 200, "jsonBody": { "status": "UP" } } }
146
+ ```
147
+
148
+ #### Mapping sources
149
+
150
+ **Local file** – a path relative to the working directory, or absolute:
151
+
152
+ ```typescript
153
+ new WiremockContainer().withMapping("health", "./test/fixtures/health.json"); // → mappings/health.json
154
+ ```
155
+
156
+ **`file:` URL** – e.g. relative to the current module:
157
+
158
+ ```typescript
159
+ new WiremockContainer().withMapping("health", new URL("./fixtures/health.json", import.meta.url));
160
+ ```
161
+
162
+ **`{ file }`** – a path or URL in object form:
163
+
164
+ ```typescript
165
+ new WiremockContainer().withMapping("health", { file: "./test/fixtures/health.json" });
166
+ ```
167
+
168
+ **`{ package, file }`** – a file inside an installed npm package, e.g. shared
169
+ fixtures. Resolved like `require()` from the working directory; the file does
170
+ not need to be exported and may not leave the package:
171
+
172
+ ```typescript
173
+ new WiremockContainer().withMapping("cards/list", { package: "@acme/api-fixtures", file: "wiremock/cards.json" }); // → mappings/cards/list.json
174
+ ```
175
+
176
+ **`{ module, file }`** – a file relative to a module, like a Java classpath
177
+ resource:
178
+
179
+ ```typescript
180
+ new WiremockContainer().withMapping("balance", { module: import.meta.url, file: "./__mocks__/balance.json" }); // ESM
181
+ new WiremockContainer().withMapping("balance", { module: __dirname, file: "./__mocks__/balance.json" }); // CJS (or __filename)
182
+ ```
183
+
184
+ **`{ json }` (object)** – an inline stub, serialised as-is:
185
+
186
+ ```typescript
187
+ new WiremockContainer().withMapping("ping", {
188
+ json: { request: { method: "GET", url: "/ping" }, response: { status: 200, body: "pong" } },
189
+ });
190
+ ```
191
+
192
+ **`{ json }` (string)** – inline JSON, copied verbatim:
193
+
194
+ ```typescript
195
+ new WiremockContainer().withMapping("ping", { json: '{ "request": { "url": "/ping" }, "response": { "status": 200 } }' });
196
+ ```
197
+
198
+ **Several stubs in one mapping** – any source may hold `{ "mappings": [...] }`:
199
+
200
+ ```typescript
201
+ new WiremockContainer().withMapping("greetings", {
202
+ json: {
203
+ mappings: [
204
+ { request: { url: "/hello" }, response: { status: 200, body: "Hello" } },
205
+ { request: { url: "/bye" }, response: { status: 200, body: "Bye" } },
206
+ ],
207
+ },
208
+ });
209
+ ```
210
+
211
+ #### Static files
212
+
213
+ `withFiles()` takes the same file sources (not `{ json }`). A file keeps its
214
+ base name unless an object source sets `name`; mappings reference it as
215
+ `bodyFileName`:
216
+
217
+ ```typescript
218
+ new WiremockContainer().withFiles(
219
+ "./test/fixtures/body-cards.json", // → __files/body-cards.json
220
+ { file: "./test/fixtures/logo.png", name: "images/logo.png" }, // → __files/images/logo.png
221
+ { package: "@acme/api-fixtures", file: "wiremock/__files/user.json" }, // → __files/user.json
222
+ );
223
+ ```
224
+
225
+ #### Combining them
226
+
227
+ All three methods chain with each other and with every Testcontainers builder:
228
+
229
+ ```typescript
230
+ const container = await new WiremockContainer()
231
+ .withMappings("./test/__mocks__/wiremock")
232
+ .withMapping("health", "./test/fixtures/health.json")
233
+ .withMapping("cards/list", { package: "@acme/api-fixtures", file: "wiremock/cards.json" })
234
+ .withFiles({ file: "./test/fixtures/cards.json", name: "cards/body.json" })
235
+ .start();
236
+ ```
237
+
238
+ Registrations are validated immediately, before any image is pulled: sources
239
+ must exist, mappings must be WireMock stubs, and no registration may overwrite a
240
+ file of another or reuse a stub `id` (WireMock would refuse to start).
241
+ Registering the same file at the same path again is a no-op. Sources are only
242
+ read – mappings are copied byte-for-byte as of registration.
243
+
91
244
  ## API
92
245
 
93
246
  ### `WiremockContainer`
94
247
 
95
248
  - `constructor(image?: string)`: Create a new container. Defaults to
96
249
  `wiremock/wiremock`.
97
- - `withMappings(directory: string)`: Mounts `mappings` and `__files` from the
98
- specified local directory to the container.
250
+ - `withMappings(directory: string)`: Copies `mappings/` (required) and
251
+ `__files/` (optional) from the specified local directory into the container.
252
+ Throws immediately if `mappings/` does not exist, so a typo fails before any
253
+ image is pulled.
254
+ - `withMapping(name: string, source: WiremockMappingSource)`: Registers one stub
255
+ at `mappings/<name>.json` (`name` may contain `/`; `.json` is optional).
256
+ - `withFiles(...files: WiremockStaticFile[])`: Registers files at
257
+ `__files/<name>` – the base name unless an object source sets `name`. A call
258
+ registers all files or none.
99
259
  - `start()`: Starts the container and returns a `StartedWiremockContainer`.
260
+ Runtime failures (no daemon, image pull denied, start-up timeout, name or
261
+ port conflicts) are propagated unchanged.
100
262
 
101
263
  ### `StartedWiremockContainer`
102
264
 
103
265
  - `rpcUrl`: Returns the base URL of the running WireMock instance (e.g.,
104
266
  `http://localhost:32768`).
105
267
  - `stop()`: Stops the container (inherited from Testcontainers).
268
+ - `[Symbol.asyncDispose]`: `await using container = await new WiremockContainer().start();`
269
+ stops the container automatically when the scope exits (TypeScript 5.2+).
106
270
 
107
271
  ## Best Practices
108
272
 
@@ -111,11 +275,127 @@ const container = await new WiremockContainer()
111
275
  2. **Mapping Organization**: Keep your mappings organized in a dedicated
112
276
  directory structure that mirrors your API endpoints.
113
277
  3. **Timeouts**: Docker container startup can sometimes exceed the default test
114
- runner timeout (e.g., 5s in Jest). Always set a higher timeout for
278
+ runner timeout (e.g. 5 s in Jest, 10 s in Vitest). Always set a higher timeout for
115
279
  `beforeAll` hooks that start containers.
116
280
  4. **Resource Management**: Always ensure `container.stop()` is called in an
117
281
  `afterAll` or `afterEach` block to prevent orphaned containers.
118
282
 
283
+ ## Development
284
+
285
+ Tooling: [Vitest](https://vitest.dev/) (tests + v8 coverage),
286
+ [oxlint](https://oxc.rs/docs/guide/usage/linter) (lint), `tsc` (typecheck)
287
+ and [tsdown](https://tsdown.dev/) (build – rolldown + oxc, emitting
288
+ declarations natively via `isolatedDeclarations` in `tsconfig.build.json`).
289
+ Node version: see `.nvmrc`.
290
+
291
+ ### Scripts
292
+
293
+ | Command | What it does | Needs Docker |
294
+ |----------------------|--------------------------------------------------------------------------|--------------|
295
+ | `pnpm build` | ESM + CJS + declarations into `dist/`, then `publint` + `attw` checks | No |
296
+ | `pnpm typecheck` | `tsc --noEmit` over `src/` and `test/` | No |
297
+ | `pnpm lint` | `oxlint --deny-warnings` with the rules in `.oxlintrc.json` | No |
298
+ | `pnpm test:unit` | `test/unit/**` – container wrapper logic, registration API and fixture integrity checks | No |
299
+ | `pnpm test:e2e` | `test/index.test.ts` – a real WireMock container serving the fixtures | **Yes** |
300
+ | `pnpm test` | Both suites, no coverage (fast local loop) | **Yes** |
301
+ | `pnpm test:coverage` | Both suites with coverage of `src/`, thresholds enforced | **Yes** |
302
+ | `pnpm check` | `build` + `typecheck` + `lint` + `test:coverage` – the full CI gate | **Yes** |
303
+
304
+ Append `--coverage` to any test script to collect coverage for that run, e.g.
305
+ `pnpm test:unit --coverage`. Coverage thresholds are **95 %** for statements,
306
+ branches, functions and lines; the unit suite alone currently reaches 100 %
307
+ and completes in well under two seconds of test time.
308
+
309
+ ### Running the E2E suite in containerised environments
310
+
311
+ The E2E suite starts a real container through Testcontainers, so the test
312
+ process needs a container runtime it can talk to. Without one, `pnpm test:e2e`,
313
+ `pnpm test` and `pnpm check` fail in the E2E suite with:
314
+
315
+ ```
316
+ Could not find a working container runtime strategy
317
+ ```
318
+
319
+ In that situation the Docker-free loop still covers build, types, lint and
320
+ 100 % of `src/`:
321
+
322
+ ```bash
323
+ pnpm build && pnpm typecheck && pnpm lint && pnpm test:unit --coverage
324
+ ```
325
+
326
+ Testcontainers resolves a runtime in this order; pick whichever your
327
+ environment can provide:
328
+
329
+ | Environment | How to enable E2E |
330
+ |-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
331
+ | GitHub Codespaces / VS Code Dev Containers | Use the provided `.devcontainer/devcontainer.json` – it adds the `docker-in-docker` feature, so a daemon runs inside the dev container. |
332
+ | Dev container with the host socket mounted | Add the `docker-outside-of-docker` feature (or mount `/var/run/docker.sock`). Set `TESTCONTAINERS_HOST_OVERRIDE=host.docker.internal` if port lookups fail. |
333
+ | Remote or rootless daemon | `DOCKER_HOST=tcp://host:2375` (or `unix:///path/to/docker.sock`). |
334
+ | Podman | `DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock` and `TESTCONTAINERS_RYUK_DISABLED=true` (or run Ryuk privileged). |
335
+ | No runtime can be installed (locked-down sandbox) | [Testcontainers Cloud](https://testcontainers.com/cloud/): run the agent and export `TC_CLOUD_TOKEN`; containers start remotely, tests run locally. |
336
+
337
+ Sandboxes that have **no runtime binary and no socket at all** (as is the case
338
+ for some hosted dev VMs) cannot run the E2E suite; the suite states this in its
339
+ header comment and `pnpm test:unit` is the supported loop there.
340
+
341
+ CI (`.github/workflows/main.yml`) runs build, typecheck, lint, unit tests
342
+ (with coverage) and E2E tests as separate steps on `ubuntu-latest`, which
343
+ ships with Docker, for every active Node LTS line.
344
+
345
+ ### Supply-chain controls
346
+
347
+ - `pnpm install --frozen-lockfile` everywhere; the lockfile is the source of truth.
348
+ - `pnpm-workspace.yaml` allow-lists the only packages permitted to run install
349
+ scripts (`ssh2`, `cpu-features`, `protobufjs` – all transitive via
350
+ `testcontainers`) and sets `minimumReleaseAge: 1440` so a version must be at
351
+ least 24 h old before it can be resolved.
352
+ - CI runs `pnpm audit --prod --audit-level=high` on every push and PR;
353
+ Dependabot (`.github/dependabot.yml`) proposes weekly updates with a 3-day
354
+ cooldown, grouping dev tooling and leaving `testcontainers` bumps individual.
355
+ - `pnpm build` runs [publint](https://publint.dev/) and
356
+ [Are the types wrong?](https://arethetypeswrong.github.io/) against the
357
+ emitted package; `size.yml` fails a PR if the tarball exceeds 25 kB.
358
+ - Releases publish with npm provenance (`--provenance`, OIDC) after a
359
+ Docker-free verify step.
360
+
361
+ ### Fixtures and schema-based assertions
362
+
363
+ The recorded WireMock session lives in `test/__mocks__/wiremock`:
364
+
365
+ - `mappings/*.json` – one stub per endpoint (`request.url`, `response.status`,
366
+ `response.headers`, `response.bodyFileName`).
367
+ - `__files/*.json` – the response bodies referenced by the mappings.
368
+
369
+ `test/helpers/fixtures.ts` loads these from disk and declares the TypeScript
370
+ shapes of every JSON payload. `test/helpers/assertions.ts` declares a
371
+ `Schema<T>` per payload – stub mappings, the admin API response and each
372
+ recorded body. The compiler requires a schema to cover **every** field of
373
+ `T`; `expectObject` then asserts each field's shape, its recorded value and
374
+ the exact key set (`partialOf` relaxes the key set for third-party shapes).
375
+
376
+ The E2E suite runs a mapping-driven contract test for every file in
377
+ `mappings/` (status, headers and full JSON body), so adding a new recorded
378
+ mapping automatically puts it under test. Endpoint-specific suites add
379
+ schema assertions, which the unit suite also runs against the fixture files
380
+ themselves to catch drift early.
381
+
382
+ Negative cases are hand-written stubs alongside the recorded ones:
383
+
384
+ - `mapping-cards-bad-request-400.json` – `GET /cards?page=0` → 400 with a
385
+ validation error envelope.
386
+ - `mapping-account-balance-server-error-500.json` – `GET /account/balance`
387
+ → 500 **only** when the request carries `X-Mock-Scenario: server-error`.
388
+ It sets `"priority": 1` so it wins over the recorded 200 stub for the same
389
+ URL; without the header the 200 stub is still served. This is the standard
390
+ WireMock pattern for simulating outages on demand.
391
+
392
+ Unmatched requests (unknown path, wrong method) get WireMock's default 404.
393
+
394
+ `test/__mocks__/standalone` holds loose fixtures for `withMapping()` and
395
+ `withFiles()`. Their unit tests need no Docker: `test/helpers/container.ts`
396
+ reads the tar archive Testcontainers would upload and asserts exactly which
397
+ files land where.
398
+
119
399
  ## Maintainers
120
400
 
121
401
  Maintained by [Hella Labs](https://hellaweb3.com/).
package/dist/index.cjs CHANGED
@@ -1,76 +1,194 @@
1
- Object.defineProperty(exports, '__esModule', { value: true });
2
-
3
- var testcontainers = require('testcontainers');
4
-
5
- /**
6
- * A Testcontainer for Wiremock.
7
- *
8
- * @example
9
- * ```typescript
10
- * const container = await new WiremockContainer().start();
11
- * ```
12
- */ class WiremockContainer extends testcontainers.GenericContainer {
13
- /**
14
- * Creates a new WiremockContainer.
15
- * @example
16
- * ```typescript
17
- * const container = await new WiremockContainer().start();
18
- * ```
19
- * @param image The docker image to use. Defaults to "wiremock/wiremock".
20
- */ constructor(image = "wiremock/wiremock"){
21
- super(image);
22
- this.withExposedPorts(8080);
23
- this.withWaitStrategy(testcontainers.Wait.forListeningPorts());
24
- }
25
- /**
26
- * Adds mappings to the Wiremock container.
27
- * @example
28
- * ```typescript
29
- * const container = await new WiremockContainer()
30
- * .withMappings("./test/__mocks__/wiremock")
31
- * .start();
32
- * ```
33
- * @param directory The directory containing the mappings and __files.
34
- */ withMappings(directory) {
35
- this.withCopyDirectoriesToContainer([
36
- {
37
- source: `${directory}/__files`,
38
- target: "/home/wiremock/__files"
39
- }
40
- ]);
41
- this.withCopyDirectoriesToContainer([
42
- {
43
- source: `${directory}/mappings`,
44
- target: "/home/wiremock/mappings"
45
- }
46
- ]);
47
- return this;
48
- }
49
- /**
50
- * Starts the container and returns a {@link StartedWiremockContainer}.
51
- * @returns A promise that resolves to the started container.
52
- */ async start() {
53
- const startedContainer = await super.start();
54
- return new StartedWiremockContainer(startedContainer, `http://${startedContainer.getHost()}:${startedContainer.getMappedPort(8080)}`);
55
- }
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_fs = require("node:fs");
3
+ let node_path = require("node:path");
4
+ let testcontainers = require("testcontainers");
5
+ let node_module = require("node:module");
6
+ let node_url = require("node:url");
7
+ // src/sources.ts
8
+ const PACKAGE_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u;
9
+ function fail(message) {
10
+ throw new Error(message);
56
11
  }
57
- /**
58
- * A started Wiremock container with helper methods for interacting with the node.
59
- */ class StartedWiremockContainer extends testcontainers.AbstractStartedContainer {
60
- /**
61
- * Creates a new StartedWiremockContainer.
62
- * @param startedTestContainer The underlying TestContainer.
63
- * @param url The URL of the started container.
64
- */ constructor(startedTestContainer, url){
65
- super(startedTestContainer);
66
- this._rpcUrl = url;
67
- }
68
- /**
69
- * Gets the URL of the Wiremock node.
70
- */ get rpcUrl() {
71
- return this._rpcUrl;
72
- }
12
+ function within(context, fn) {
13
+ try {
14
+ return fn();
15
+ } catch (error) {
16
+ throw new Error(`${context}: ${error.message}`, { cause: error });
17
+ }
73
18
  }
74
-
19
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
20
+ const show = (value) => value instanceof URL ? value.href : JSON.stringify(value) ?? String(value);
21
+ const text = (value, field) => typeof value === "string" && value.trim() !== "" ? value : fail(`"${field}" must be a non-empty string, got ${show(value)}`);
22
+ const toPath = (value, field) => {
23
+ if (!(value instanceof URL) && !text(value, field).startsWith("file:")) return (0, node_path.resolve)(value);
24
+ try {
25
+ return (0, node_url.fileURLToPath)(value);
26
+ } catch {
27
+ return fail(`"${field}" must be a path or a file: URL, got ${show(value)}`);
28
+ }
29
+ };
30
+ const existingFile = (path) => {
31
+ const stats = (0, node_fs.statSync)(path, { throwIfNoEntry: false });
32
+ return stats?.isFile() ? path : fail(`"${path}" ${stats ? "is not a file" : "does not exist"}`);
33
+ };
34
+ const packageRoot = (name) => {
35
+ if (!PACKAGE_NAME.test(name)) fail(`"${name}" is not a valid npm package name`);
36
+ return ((0, node_module.createRequire)((0, node_path.join)(process.cwd(), "noop.js")).resolve.paths(name) ?? []).map((dir) => (0, node_path.join)(dir, name)).find((dir) => (0, node_fs.existsSync)((0, node_path.join)(dir, "package.json"))) ?? fail(`package "${name}" is not installed (searched from "${process.cwd()}")`);
37
+ };
38
+ function resolveFile(source) {
39
+ if (typeof source === "string" || source instanceof URL) return existingFile(toPath(source, "source"));
40
+ if (!isRecord(source)) return fail(`unsupported source ${show(source)}; expected a path, a file: URL, { file }, { package, file } or { module, file }`);
41
+ const { file, package: pkg, module } = source;
42
+ if (pkg !== void 0 && module !== void 0) fail("a source may set \"package\" or \"module\", not both");
43
+ if (pkg !== void 0) {
44
+ const root = packageRoot(text(pkg, "package"));
45
+ const path = (0, node_path.resolve)(root, text(file, "file"));
46
+ return (0, node_path.relative)(root, path).split(node_path.sep)[0] === ".." ? fail(`"${String(file)}" must point to a file inside package "${pkg}"`) : existingFile(path);
47
+ }
48
+ if (module === void 0) return existingFile(toPath(file, "file"));
49
+ const anchor = toPath(module, "module");
50
+ return existingFile((0, node_path.resolve)((0, node_fs.statSync)(anchor, { throwIfNoEntry: false })?.isDirectory() ? anchor : (0, node_path.dirname)(anchor), text(file, "file")));
51
+ }
52
+ const containerName = (name) => {
53
+ const value = text(name, "name");
54
+ return value.split("/").some((part) => [
55
+ "",
56
+ ".",
57
+ ".."
58
+ ].includes(part) || /[\\\0]/u.test(part)) ? fail(`name "${value}" must be a relative path using "/" separators, without empty, "." or ".." segments`) : value;
59
+ };
60
+ const inlineJson = (json) => typeof json === "string" ? json : JSON.stringify(json, null, 2) ?? fail("\"json\" must be a JSON string or an object");
61
+ const parseJson = (json) => {
62
+ try {
63
+ return JSON.parse(json);
64
+ } catch (error) {
65
+ return fail(`invalid JSON (${error.message})`);
66
+ }
67
+ };
68
+ function stubIds(json) {
69
+ const value = parseJson(json);
70
+ const many = isRecord(value) && "mappings" in value;
71
+ const stubs = many ? value.mappings : [value];
72
+ if (!Array.isArray(stubs) || stubs.length === 0) fail("\"mappings\" must be a non-empty array of stub mappings");
73
+ return stubs.flatMap((stub, index) => {
74
+ if (!isRecord(stub) || !isRecord(stub.request) || !isRecord(stub.response)) fail(`${many ? `mappings[${index}]` : "the mapping"} is not a WireMock stub mapping ("request" and "response" objects are required)`);
75
+ const id = stub.id ?? stub.uuid;
76
+ return typeof id === "string" ? [id] : [];
77
+ });
78
+ }
79
+ const stubIdsOf = (path) => {
80
+ try {
81
+ return path.endsWith(".json") ? stubIds((0, node_fs.readFileSync)(path, "utf8")) : [];
82
+ } catch {
83
+ return [];
84
+ }
85
+ };
86
+ const listFiles = (dir) => (0, node_fs.readdirSync)(dir, {
87
+ recursive: true,
88
+ withFileTypes: true
89
+ }).filter((entry) => !entry.isDirectory()).map((entry) => (0, node_path.relative)(dir, (0, node_path.join)(entry.parentPath, entry.name)).split(node_path.sep).join("/"));
90
+ // src/index.ts
91
+ const MAPPINGS = "/home/wiremock/mappings";
92
+ const FILES = "/home/wiremock/__files";
93
+ const INLINE = "inline JSON";
94
+ var WiremockContainer = class extends testcontainers.GenericContainer {
95
+ #origins = /* @__PURE__ */ new Map();
96
+ #stubIds = /* @__PURE__ */ new Map();
97
+ constructor(image = "wiremock/wiremock") {
98
+ super(image);
99
+ this.withExposedPorts(8080);
100
+ this.withWaitStrategy(testcontainers.Wait.forListeningPorts());
101
+ }
102
+ withMappings(directory) {
103
+ within("WiremockContainer.withMappings", () => {
104
+ const mappings = (0, node_path.join)(directory, "mappings");
105
+ if (!(0, node_fs.existsSync)(mappings)) fail(`"${mappings}" does not exist`);
106
+ if (!(0, node_fs.statSync)(mappings).isDirectory()) fail(`"${mappings}" is not a directory`);
107
+ const files = (0, node_path.join)(directory, "__files");
108
+ const dirs = [{
109
+ source: mappings,
110
+ target: MAPPINGS
111
+ }, ...(0, node_fs.existsSync)(files) ? [{
112
+ source: files,
113
+ target: FILES
114
+ }] : []];
115
+ this.#claim(dirs.flatMap(({ source, target }) => listFiles(source).map((name) => ({
116
+ target: node_path.posix.join(target, name),
117
+ origin: (0, node_path.resolve)(source, name),
118
+ ids: target === MAPPINGS ? stubIdsOf((0, node_path.join)(source, name)) : []
119
+ }))));
120
+ this.withCopyDirectoriesToContainer(dirs);
121
+ });
122
+ return this;
123
+ }
124
+ withMapping(name, source) {
125
+ within(`WiremockContainer.withMapping("${name}")`, () => {
126
+ const file = containerName(name);
127
+ const target = node_path.posix.join(MAPPINGS, file.endsWith(".json") ? file : `${file}.json`);
128
+ const inline = isRecord(source) && "json" in source;
129
+ const origin = inline ? INLINE : resolveFile(source);
130
+ const content = inline ? inlineJson(source.json) : (0, node_fs.readFileSync)(origin);
131
+ const ids = within(origin, () => stubIds(content.toString()));
132
+ this.withCopyContentToContainer(this.#claim([{
133
+ target,
134
+ origin,
135
+ ids
136
+ }]).map(() => ({
137
+ content,
138
+ target
139
+ })));
140
+ });
141
+ return this;
142
+ }
143
+ withFiles(...files) {
144
+ within("WiremockContainer.withFiles", () => {
145
+ const claims = files.map((file) => {
146
+ const origin = resolveFile(file);
147
+ const name = isRecord(file) && file.name !== void 0 ? containerName(file.name) : (0, node_path.basename)(origin);
148
+ return {
149
+ target: node_path.posix.join(FILES, name),
150
+ origin
151
+ };
152
+ });
153
+ this.withCopyFilesToContainer(this.#claim(claims).map(({ origin, target }) => ({
154
+ source: origin,
155
+ target
156
+ })));
157
+ });
158
+ return this;
159
+ }
160
+ async start() {
161
+ const startedContainer = await super.start();
162
+ return new StartedWiremockContainer(startedContainer, `http://${startedContainer.getHost()}:${startedContainer.getMappedPort(8080)}`);
163
+ }
164
+ #claim(claims) {
165
+ const origins = new Map(this.#origins);
166
+ const owners = new Map(this.#stubIds);
167
+ const fresh = claims.filter(({ target, origin, ids = [] }) => {
168
+ const existing = origins.get(target);
169
+ if (existing === origin && origin !== INLINE) return false;
170
+ if (existing) fail(`"${target}" is already registered from ${existing}; registering ${origin} would overwrite it`);
171
+ for (const id of ids) {
172
+ const owner = owners.get(id);
173
+ if (owner) fail(`stub id "${id}" in ${origin} is already used by "${owner}"; WireMock refuses to start with duplicate stub ids`);
174
+ owners.set(id, target);
175
+ }
176
+ origins.set(target, origin);
177
+ return true;
178
+ });
179
+ [this.#origins, this.#stubIds] = [origins, owners];
180
+ return fresh;
181
+ }
182
+ };
183
+ var StartedWiremockContainer = class extends testcontainers.AbstractStartedContainer {
184
+ #rpcUrl;
185
+ constructor(startedTestContainer, url) {
186
+ super(startedTestContainer);
187
+ this.#rpcUrl = url;
188
+ }
189
+ get rpcUrl() {
190
+ return this.#rpcUrl;
191
+ }
192
+ };
75
193
  exports.StartedWiremockContainer = StartedWiremockContainer;
76
194
  exports.WiremockContainer = WiremockContainer;