@hellaweb3/wiremock-testcontainers-nodejs 0.3.0 → 0.4.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 +52 -8
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +100 -2
- package/dist/index.d.ts +100 -2
- package/dist/index.js +1 -1
- package/package.json +7 -5
- package/dist/no-docker.cjs +0 -1
- package/dist/no-docker.d.cts +0 -45
- package/dist/no-docker.d.ts +0 -45
- package/dist/no-docker.js +0 -1
- package/dist/runtime-6blQsklJ.cjs +0 -1
- package/dist/runtime-ChyDTLYb.js +0 -1
package/README.md
CHANGED
|
@@ -74,6 +74,48 @@ new WiremockContainer("wiremock/wiremock:3.13.0") // default: wiremock/wiremock
|
|
|
74
74
|
Registrations are validated before any image is pulled: sources must exist, mappings must be
|
|
75
75
|
WireMock stubs, and nothing may overwrite another registration or reuse a stub `id`.
|
|
76
76
|
|
|
77
|
+
## Extensions
|
|
78
|
+
|
|
79
|
+
Load [WireMock extensions](https://wiremock.org/docs/extending-wiremock/) from compiled JARs – no JDK needed:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
new WiremockContainer()
|
|
83
|
+
.withExtensionJar("./wiremock/acme.jar") // onto WireMock's class path
|
|
84
|
+
.withExtension("com.acme.UppercaseTransformer") // --extensions com.acme.UppercaseTransformer
|
|
85
|
+
.withMapping("shout", { json: { request: { url: "/shout" }, response: { body: "hi", transformers: ["uppercase"] } } });
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| Method | Registers |
|
|
89
|
+
|--------------------------------------------------------|---------------------------------------------------------------------------|
|
|
90
|
+
| `withExtension(ext)`, `withExtensions([...])` | A fully qualified class name, or a [TypeScript extension](#typescript-extensions) |
|
|
91
|
+
| `withExtensionJar(jar)`, `withExtensionJars([...])` | A JAR, or every `*.jar` of a directory – any source above; `{ file, name }` renames |
|
|
92
|
+
| `withExtensionOptions({ scanning, verifyClassNames })` | `scanning: false` ignores JARs' `META-INF/services`; `verifyClassNames: false` skips the class check |
|
|
93
|
+
|
|
94
|
+
- Class names share one `--extensions` flag, in registration order, without repeats.
|
|
95
|
+
- Validated before start: JARs must be readable `*.jar`s; each class must be in a registered JAR
|
|
96
|
+
or WireMock itself (else: *did you mean …?*); `withCommand()` must not repeat `--extensions` or
|
|
97
|
+
an extension's option – WireMock crashes on repeats. Start-up failures name the extensions.
|
|
98
|
+
- WireMock 3 runs on Java 11+, so JARs must target Java 11. Custom images must keep the official
|
|
99
|
+
`/var/wiremock/extensions` class path.
|
|
100
|
+
|
|
101
|
+
### TypeScript extensions
|
|
102
|
+
|
|
103
|
+
Bundle class names, JARs and options; register the class or an instance:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import type { WireMockExtension } from "@hellaweb3/wiremock-testcontainers-nodejs";
|
|
107
|
+
|
|
108
|
+
class Uppercase implements WireMockExtension {
|
|
109
|
+
getName() { return "uppercase"; } // identifies it; implement at least one method below
|
|
110
|
+
getClassNames() { return ["com.acme.UppercaseTransformer"]; }
|
|
111
|
+
getJars() { return [{ module: import.meta.url, file: "./acme.jar" }]; }
|
|
112
|
+
getOptions() { return ["--global-response-templating"]; } // "--flag" or "--option=value"
|
|
113
|
+
}
|
|
114
|
+
new WiremockContainer().withExtension(Uppercase);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The [guide for extension authors](./examples/extensions/README.md) has recipes, tests and publishing.
|
|
118
|
+
|
|
77
119
|
## Test utilities
|
|
78
120
|
|
|
79
121
|
`start()` resolves to a `StartedWiremockContainer`:
|
|
@@ -102,8 +144,10 @@ const [request] = await wiremock.findRequests({
|
|
|
102
144
|
expect(JSON.parse(request.body ?? "")).toMatchObject({ cardType: "virtual" });
|
|
103
145
|
```
|
|
104
146
|
|
|
105
|
-
Also exported: `WaitUntilReadyOptions`,
|
|
106
|
-
`WiremockStaticFile`, `WiremockFileSource`, `WiremockPath` and `WiremockStubMapping
|
|
147
|
+
Also exported: `WaitUntilReadyOptions`, the source types `WiremockMappingSource`,
|
|
148
|
+
`WiremockStaticFile`, `WiremockFileSource`, `WiremockPath` and `WiremockStubMapping`, and the
|
|
149
|
+
extension types `WireMockExtension`, `ExtensionConstructor`, `ExtensionInput`,
|
|
150
|
+
`WiremockExtensionJar` and `WiremockExtensionOptions`.
|
|
107
151
|
|
|
108
152
|
**Tips:** start one container per suite and `reset()` between tests, which is far faster than
|
|
109
153
|
restarting; raise the `beforeAll` timeout for the first image pull; assert with
|
|
@@ -112,8 +156,8 @@ restarting; raise the `beforeAll` timeout for the first image pull; assert with
|
|
|
112
156
|
## Without Docker (local development only)
|
|
113
157
|
|
|
114
158
|
No container runtime? With Java 11+, `@hellaweb3/wiremock-testcontainers-nodejs/no-docker`
|
|
115
|
-
serves every `WiremockContainer` from a local WireMock JVM. Tests stay unchanged: stubs, files
|
|
116
|
-
and `withCommand()` flags apply as in the container.
|
|
159
|
+
serves every `WiremockContainer` from a local WireMock JVM. Tests stay unchanged: stubs, files,
|
|
160
|
+
extensions and `withCommand()` flags apply as in the container.
|
|
117
161
|
|
|
118
162
|
> [!WARNING]
|
|
119
163
|
> Never the default, never for CI/CD: it refuses to run when CI is detected (`CI`,
|
|
@@ -149,10 +193,10 @@ Set `WIREMOCK_NO_DOCKER=1` from a `--no-docker` script flag – see
|
|
|
149
193
|
- **Speed:** a C1-only JIT and a class-data archive per JAR and Java in `cacheDir`. The first run
|
|
150
194
|
records it (a slower `stop()`); later runs boot ~2× faster and stop instantly. `JAVA_OPTS`
|
|
151
195
|
override the JVM flags.
|
|
152
|
-
- **Differences:** WireMock listens on `127.0.0.1`;
|
|
153
|
-
throw; leftover JVMs are killed on exit.
|
|
154
|
-
- **Also exported
|
|
155
|
-
whether `NO_DOCKER_ENV` (`WIREMOCK_NO_DOCKER`) is `1` or `true`.
|
|
196
|
+
- **Differences:** WireMock listens on `127.0.0.1`; extension JARs join its class path;
|
|
197
|
+
`exec()`, networks and copying after start throw; leftover JVMs are killed on exit.
|
|
198
|
+
- **Also exported** (from `…/no-docker` and the package root): `detectCI(env?)` – the CI variable
|
|
199
|
+
set, if any; `isNoDockerRequested(env?)` – whether `NO_DOCKER_ENV` (`WIREMOCK_NO_DOCKER`) is `1` or `true`.
|
|
156
200
|
|
|
157
201
|
## More
|
|
158
202
|
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const require_runtime=require("./runtime-6blQsklJ.cjs");let node_fs=require("node:fs");let node_path=require("node:path");let testcontainers=require("testcontainers");let node_module=require("node:module");let node_url=require("node:url");const PACKAGE_NAME=/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u;function fail(message){throw new Error(message)}function within(context,fn){try{return fn()}catch(error){throw new Error(`${context}: ${error.message}`,{cause:error})}}const isRecord=value=>typeof value===`object`&&value!==null&&!Array.isArray(value);const show=value=>value instanceof URL?value.href:JSON.stringify(value)??String(value);const text=(value,field)=>typeof value===`string`&&value.trim()!==``?value:fail(`"${field}" must be a non-empty string, got ${show(value)}`);const toPath=(value,field)=>{if(!(value instanceof URL)&&!text(value,field).startsWith(`file:`))return(0,node_path.resolve)(value);try{return(0,node_url.fileURLToPath)(value)}catch{return fail(`"${field}" must be a path or a file: URL, got ${show(value)}`)}};const existingFile=path=>{const stats=(0,node_fs.statSync)(path,{throwIfNoEntry:false});return stats?.isFile()?path:fail(`"${path}" ${stats?`is not a file`:`does not exist`}`)};const packageRoot=name=>{if(!PACKAGE_NAME.test(name))fail(`"${name}" is not a valid npm package name`);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()}")`)};function resolveFile(source){if(typeof source===`string`||source instanceof URL)return existingFile(toPath(source,`source`));if(!isRecord(source))return fail(`unsupported source ${show(source)}; expected a path, a file: URL, { file }, { package, file } or { module, file }`);const{file,package:pkg,module}=source;if(pkg!==void 0&&module!==void 0)fail(`a source may set "package" or "module", not both`);if(pkg!==void 0){const root=packageRoot(text(pkg,`package`));const path=(0,node_path.resolve)(root,text(file,`file`));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)}if(module===void 0)return existingFile(toPath(file,`file`));const anchor=toPath(module,`module`);return existingFile((0,node_path.resolve)((0,node_fs.statSync)(anchor,{throwIfNoEntry:false})?.isDirectory()?anchor:(0,node_path.dirname)(anchor),text(file,`file`)))}const containerName=name=>{const value=text(name,`name`);return value.split(`/`).some(part=>[``,`.`,`..`].includes(part)||/[\\\0]/u.test(part))?fail(`name "${value}" must be a relative path using "/" separators, without empty, "." or ".." segments`):value};const inlineJson=json=>typeof json===`string`?json:JSON.stringify(json,null,2)??fail(`"json" must be a JSON string or an object`);const parseJson=json=>{try{return JSON.parse(json)}catch(error){return fail(`invalid JSON (${error.message})`)}};function stubIds(json){const value=parseJson(json);const many=isRecord(value)&&`mappings`in value;const stubs=many?value.mappings:[value];if(!Array.isArray(stubs)||stubs.length===0)fail(`"mappings" must be a non-empty array of stub mappings`);return stubs.flatMap((stub,index)=>{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)`);const id=stub.id??stub.uuid;return typeof id===`string`?[id]:[]})}const stubIdsOf=path=>{try{return path.endsWith(`.json`)?stubIds((0,node_fs.readFileSync)(path,`utf8`)):[]}catch{return[]}};const listFiles=dir=>(0,node_fs.readdirSync)(dir,{recursive:true,withFileTypes:true}).filter(entry=>!entry.isDirectory()).map(entry=>(0,node_path.relative)(dir,(0,node_path.join)(entry.parentPath,entry.name)).split(node_path.sep).join(`/`));const MAPPINGS=`/home/wiremock/mappings`;const FILES=`/home/wiremock/__files`;const INLINE=`inline JSON`;var WiremockContainer=class extends testcontainers.GenericContainer{#origins=/* @__PURE__ */ new Map;#stubIds=/* @__PURE__ */ new Map;constructor(image=`wiremock/wiremock`){super(image);this.withExposedPorts(8080);this.withWaitStrategy(testcontainers.Wait.forListeningPorts())}withMappings(directory){within(`WiremockContainer.withMappings`,()=>{const mappings=(0,node_path.join)(directory,`mappings`);if(!(0,node_fs.existsSync)(mappings))fail(`"${mappings}" does not exist`);if(!(0,node_fs.statSync)(mappings).isDirectory())fail(`"${mappings}" is not a directory`);const files=(0,node_path.join)(directory,`__files`);const dirs=[{source:mappings,target:MAPPINGS},...(0,node_fs.existsSync)(files)?[{source:files,target:FILES}]:[]];this.#claim(dirs.flatMap(({source,target})=>listFiles(source).map(name=>({target:node_path.posix.join(target,name),origin:(0,node_path.resolve)(source,name),ids:target===MAPPINGS?stubIdsOf((0,node_path.join)(source,name)):[]}))));this.withCopyDirectoriesToContainer(dirs)});return this}withMapping(name,source){within(`WiremockContainer.withMapping("${name}")`,()=>{const file=containerName(name);const target=node_path.posix.join(MAPPINGS,file.endsWith(`.json`)?file:`${file}.json`);const inline=isRecord(source)&&`json`in source;const origin=inline?INLINE:resolveFile(source);const content=inline?inlineJson(source.json):(0,node_fs.readFileSync)(origin);const ids=within(origin,()=>stubIds(content.toString()));this.withCopyContentToContainer(this.#claim([{target,origin,ids}]).map(()=>({content,target})))});return this}withFiles(...files){within(`WiremockContainer.withFiles`,()=>{const claims=files.map(file=>{const origin=resolveFile(file);const name=isRecord(file)&&file.name!==void 0?containerName(file.name):(0,node_path.basename)(origin);return{target:node_path.posix.join(FILES,name),origin}});this.withCopyFilesToContainer(this.#claim(claims).map(({origin,target})=>({source:origin,target})))});return this}async start(){const runtime=require_runtime.t();const startedContainer=runtime?await runtime.start(this.#startRequest()):await super.start();return new StartedWiremockContainer(startedContainer,`http://${startedContainer.getHost()}:${startedContainer.getMappedPort(8080)}`)}#startRequest(){return{image:this.imageName.string,name:this.createOpts.name,command:[...this.createOpts.Cmd??[]],environment:{...this.environment},directories:this.directoriesToCopy.map(({source,target})=>({source,target})),files:this.filesToCopy.map(({source,target})=>({source,target})),contents:this.contentsToCopy.map(({content,target})=>({content,target}))}}#claim(claims){const origins=new Map(this.#origins);const owners=new Map(this.#stubIds);const fresh=claims.filter(({target,origin,ids=[]})=>{const existing=origins.get(target);if(existing===origin&&origin!==INLINE)return false;if(existing)fail(`"${target}" is already registered from ${existing}; registering ${origin} would overwrite it`);for(const id of ids){const owner=owners.get(id);if(owner)fail(`stub id "${id}" in ${origin} is already used by "${owner}"; WireMock refuses to start with duplicate stub ids`);owners.set(id,target)}origins.set(target,origin);return true});[this.#origins,this.#stubIds]=[origins,owners];return fresh}};const ADMIN_PATH=`/__admin`;var StartedWiremockContainer=class extends testcontainers.AbstractStartedContainer{#rpcUrl;constructor(startedTestContainer,url){super(startedTestContainer);this.#rpcUrl=url}get rpcUrl(){return this.#rpcUrl}getUrl(path=``){if(path===``)return this.#rpcUrl;const suffix=path.startsWith(`/`)||path.startsWith(`?`)?path:`/${path}`;return`${this.#rpcUrl}${suffix}`}getAdminUrl(){return`${this.#rpcUrl}${ADMIN_PATH}`}getMappingUrl(){return`${this.getAdminUrl()}/mappings`}async isRunning(){try{const health=await fetch(`${this.getAdminUrl()}/health`);if(health.status!==404)return health.ok;return(await fetch(this.getMappingUrl())).ok}catch{return false}}async waitUntilReady(options={}){const{timeout=3e4,interval=250}=options;const deadline=Date.now()+timeout;const poll=async()=>{if(await this.isRunning())return;if(Date.now()>=deadline)throw new Error(`WireMock at ${this.#rpcUrl} was not ready within ${timeout}ms`);await new Promise(_resolve=>setTimeout(_resolve,interval));return poll()};await poll()}async reset(){await this.#admin(`POST`,`/reset`)}async resetRequests(){await this.#admin(`DELETE`,`/requests`)}async countRequests(request){const{count}=await(await this.#admin(`POST`,`/requests/count`,request)).json();return count}async findRequests(request){const{requests}=await(await this.#admin(`POST`,`/requests/find`,request)).json();return requests??[]}async#admin(method,path,body){const response=await fetch(`${this.getAdminUrl()}${path}`,{method,...body===void 0?{}:{headers:{"Content-Type":`application/json`},body:JSON.stringify(body)}});if(!response.ok){const detail=await response.text().catch(()=>``);throw new Error(`WireMock admin request ${method} ${ADMIN_PATH}${path} failed with ${response.status} ${response.statusText}${detail?`: ${detail}`:``}`)}return response}};exports.StartedWiremockContainer=StartedWiremockContainer;exports.WiremockContainer=WiremockContainer;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("node:fs"),t=require("node:path"),n=require("testcontainers"),r=require("node:zlib"),i=require("node:module"),a=require("node:url"),o=require("node:child_process"),s=require("node:crypto"),c=require("node:events"),l=require("node:net"),u=require("node:os"),d=require("node:stream"),f=require("node:stream/consumers");const p=Buffer.from(`PK`,`latin1`);function jarEntries(e){let t=e.length<22?-1:e.lastIndexOf(p,e.length-22);if(t<0||t<e.length-22-65535)throw Error(`not a ZIP archive`);let n=e.readUInt16LE(t+10),r=e.readUInt32LE(t+16),i=t-20;if(i>=0&&e.readUInt32LE(i)===117853008){let t=Number(e.readBigUInt64LE(i+8));n=Number(e.readBigUInt64LE(t+32)),r=Number(e.readBigUInt64LE(t+48))}let a=[];for(let t=0;t<n;t++){if(r+46>e.length||e.readUInt32LE(r)!==33639248)throw Error(`corrupt ZIP central directory`);let t=e.readUInt16LE(r+28),n=e.toString(`utf8`,r+46,r+46+t);a.push({name:n,method:e.readUInt16LE(r+10),size:e.readUInt32LE(r+20),offset:e.readUInt32LE(r+42)}),r+=46+t+e.readUInt16LE(r+30)+e.readUInt16LE(r+32)}return a}function jarEntry(e,t){try{let n=jarEntries(e).find(e=>e.name===t);if(!n||e.readUInt32LE(n.offset)!==67324752)return;let i=n.offset+30+e.readUInt16LE(n.offset+26)+e.readUInt16LE(n.offset+28),a=e.subarray(i,i+n.size);return n.method===0?a:n.method===8?(0,r.inflateRawSync)(a):void 0}catch{return}}function mainClass(e){let t=jarEntry(e,`META-INF/MANIFEST.MF`)?.toString(`utf8`);return t&&/^Main-Class:\s*(\S+)\s*$/mu.exec(t)?.[1]}const m=`/var/wiremock/extensions`,h=Symbol.for(`@hellaweb3/wiremock-testcontainers-nodejs/runtime`),g=globalThis,getRuntime=()=>g[h],setRuntime=e=>{e?g[h]=e:delete g[h]},_=/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u;function fail(e){throw Error(e)}function within(e,t){try{return t()}catch(t){throw Error(`${e}: ${t.message}`,{cause:t})}}const isRecord=e=>typeof e==`object`&&!!e&&!Array.isArray(e),show=e=>e instanceof URL?e.href:JSON.stringify(e)??String(e),text=(e,t)=>typeof e==`string`&&e.trim()!==``?e:fail(`"${t}" must be a non-empty string, got ${show(e)}`),toPath=(e,n)=>{if(!(e instanceof URL)&&!text(e,n).startsWith(`file:`))return(0,t.resolve)(e);try{return(0,a.fileURLToPath)(e)}catch{return fail(`"${n}" must be a path or a file: URL, got ${show(e)}`)}},existingFile=t=>{let n=(0,e.statSync)(t,{throwIfNoEntry:!1});return n?.isFile()?t:fail(`"${t}" ${n?`is not a file`:`does not exist`}`)},packageRoot=n=>(_.test(n)||fail(`"${n}" is not a valid npm package name`),((0,i.createRequire)((0,t.join)(process.cwd(),`noop.js`)).resolve.paths(n)??[]).map(e=>(0,t.join)(e,n)).find(n=>(0,e.existsSync)((0,t.join)(n,`package.json`)))??fail(`package "${n}" is not installed (searched from "${process.cwd()}")`));function resolvePath$1(n){if(typeof n==`string`||n instanceof URL)return toPath(n,`source`);if(!isRecord(n))return fail(`unsupported source ${show(n)}; expected a path, a file: URL, { file }, { package, file } or { module, file }`);let{file:r,package:i,module:a}=n;if(i!==void 0&&a!==void 0&&fail(`a source may set "package" or "module", not both`),i!==void 0){let e=packageRoot(text(i,`package`)),n=(0,t.resolve)(e,text(r,`file`));return(0,t.relative)(e,n).split(t.sep)[0]===`..`?fail(`"${String(r)}" must point to a file inside package "${i}"`):n}if(a===void 0)return toPath(r,`file`);let o=toPath(a,`module`);return(0,t.resolve)((0,e.statSync)(o,{throwIfNoEntry:!1})?.isDirectory()?o:(0,t.dirname)(o),text(r,`file`))}const resolveFile=e=>existingFile(resolvePath$1(e)),containerName=e=>{let t=text(e,`name`);return t.split(`/`).some(e=>[``,`.`,`..`].includes(e)||/[\\\0]/u.test(e))?fail(`name "${t}" must be a relative path using "/" separators, without empty, "." or ".." segments`):t},inlineJson=e=>typeof e==`string`?e:JSON.stringify(e,null,2)??fail(`"json" must be a JSON string or an object`),parseJson=e=>{try{return JSON.parse(e)}catch(e){return fail(`invalid JSON (${e.message})`)}};function stubIds(e){let t=parseJson(e),n=isRecord(t)&&`mappings`in t,r=n?t.mappings:[t];return Array.isArray(r)&&r.length!==0||fail(`"mappings" must be a non-empty array of stub mappings`),r.flatMap((e,t)=>{isRecord(e)&&isRecord(e.request)&&isRecord(e.response)||fail(`${n?`mappings[${t}]`:`the mapping`} is not a WireMock stub mapping ("request" and "response" objects are required)`);let r=e.id??e.uuid;return typeof r==`string`?[r]:[]})}const stubIdsOf=t=>{try{return t.endsWith(`.json`)?stubIds((0,e.readFileSync)(t,`utf8`)):[]}catch{return[]}},listFiles=n=>(0,e.readdirSync)(n,{recursive:!0,withFileTypes:!0}).filter(e=>!e.isDirectory()).map(e=>(0,t.relative)(n,(0,t.join)(e.parentPath,e.name)).split(t.sep).join(`/`)),v=String.raw`[\p{L}_$][\p{L}\p{N}_$]*`,y=RegExp(`^${v}(?:\\.${v})+$`,`u`),b=/^--[a-z\d][\w-]*(?:=.*)?$/iu,x=[`com.github.tomakehurst.wiremock.`,`org.wiremock.`];function className(e){let t=text(e,`class name`).trim();return y.test(t)&&!t.endsWith(`.class`)?t:fail(`"${t}" is not a fully qualified Java class name such as "com.acme.MyTransformer" (${t.endsWith(`.class`)?`drop the ".class" suffix`:/[/\\]/u.test(t)?`separate packages with "." not "/"`:t.includes(`.`)?`only letters, digits, _ and $ may form each part`:`include the package – an extension's short name is not enough`})`)}function inspectJar(n,r){r.includes(`/`)&&fail(`name "${r}" must be a file name: WireMock's class path holds only the JARs directly in ${m}`),/\.jar$/iu.test(r)||fail(`"${r}" must end in ".jar" – WireMock's class path picks up only *.jar files; pass { file, name: "<name>.jar" } to rename it`);let i;try{i=(0,e.readFileSync)(n)}catch(e){return fail(`"${n}" cannot be read: ${e.message}`)}let a=within(`"${n}" is not a valid JAR`,()=>jarEntries(i)).map(e=>e.name),o=a.filter(e=>e.endsWith(`.class`)&&!e.startsWith(`META-INF/`)).map(e=>e.slice(0,-6).replaceAll(`/`,`.`));return o.length===0&&!a.some(e=>e.startsWith(`META-INF/services/`))&&fail(`"${n}" contains no Java classes – is it really an extension JAR?`),{target:t.posix.join(m,r),origin:n,classes:o}}function resolveJars(n){let r=resolvePath$1(n),i=(0,e.statSync)(r,{throwIfNoEntry:!1})??fail(`"${r}" does not exist`),a=isRecord(n)?n.name:void 0;if(i.isDirectory()){a!==void 0&&fail(`"name" cannot rename the directory "${r}", only a single JAR`);let n=(0,e.readdirSync)(r,{withFileTypes:!0}).filter(e=>e.isFile()&&/\.jar$/iu.test(e.name));return n.length===0&&fail(`the directory "${r}" contains no *.jar files`),n.map(e=>e.name).toSorted().map(e=>inspectJar((0,t.join)(r,e),e))}return i.isFile()||fail(`"${r}" is not a file`),[inspectJar(r,a===void 0?(0,t.basename)(r):text(a,`name`))]}function list(e,t){let n=e[t];if(n===void 0)return[];typeof n!=`function`&&fail(`${t} must be a method, got ${show(n)}`);let r=n.call(e);return Array.isArray(r)?r:fail(`${t}() must return an array, got ${show(r)}`)}function contribution(e){if(typeof e==`string`)return{classNames:[className(e)],jars:[],options:[]};let t=e;if(typeof e==`function`)try{t=new e}catch(t){return fail(`could not instantiate the extension class ${e.name||`(anonymous)`}: ${t.message}`)}if(!isRecord(t)||typeof t.getName!=`function`)return fail(`unsupported extension ${show(e)}; expected a fully qualified Java class name, a WireMockExtension or its class`);let n=t,r=text(n.getName(),`getName()`);return within(`extension "${r}"`,()=>{let e=list(n,`getOptions`).map(e=>typeof e==`string`&&b.test(e)?e:fail(`option ${show(e)} must be "--flag" or "--option=value", one option per entry`));e.some(e=>optionName(e)===`--extensions`)&&fail(`set class names with getClassNames(), not "--extensions"`);let t=list(n,`getClassNames`).map(className),i=list(n,`getJars`).flatMap(resolveJars);return t.length+i.length+e.length===0&&fail(`it contributes nothing: implement getClassNames(), getJars() or getOptions()`),{name:r,classNames:t,jars:i,options:e}})}const optionName=e=>e.split(`=`,1)[0];function mergeOptions(e,t,n){let r=[...e];for(let e of t){if(r.includes(e))continue;let t=r.find(t=>optionName(t)===optionName(e));t&&fail(`option "${e}" conflicts with "${t}" set by ${n}; WireMock accepts each option only once`),r.push(e)}return r}const simpleName=e=>e.slice(Math.max(e.lastIndexOf(`.`),e.lastIndexOf(`$`))+1).toLowerCase();function verifyClassNames(e,n){for(let r of e){if(x.some(e=>r.startsWith(e))||n.some(e=>e.classes.includes(r)))continue;let e=n.flatMap(e=>e.classes.filter(e=>simpleName(e)===simpleName(r)));fail(`class "${r}" is in ${n.length?`none of the registered extension JARs (${n.map(e=>(0,t.basename)(e.target)).join(`, `)})`:`no JAR: none is registered`}${e.length?`; did you mean "${e.join(`" or "`)}"?`:`.`} Register its JAR with withExtensionJar(), or – if the image already provides it – skip this check with withExtensionOptions({ verifyClassNames: false })`)}}const S=`WIREMOCK_NO_DOCKER`,C=[`CI`,`CONTINUOUS_INTEGRATION`,`GITHUB_ACTIONS`,`GITLAB_CI`,`CIRCLECI`,`BUILDKITE`,`JENKINS_URL`,`TF_BUILD`,`TEAMCITY_VERSION`,`BITBUCKET_BUILD_NUMBER`,`CODEBUILD_BUILD_ID`],w=`127.0.0.1`,T=[`-XX:+IgnoreUnrecognizedVMOptions`,`-XX:TieredStopAtLevel=1`];function detectCI(e=process.env){return C.find(t=>{let n=e[t]?.trim().toLowerCase();return n!==void 0&&n!==``&&n!==`false`&&n!==`0`})}function isNoDockerRequested(e=process.env){let t=e[S]?.trim().toLowerCase();return t===`1`||t===`true`}function installNoDockerRuntime(e={}){let n=e.env??process.env,r=detectCI(n);if(r)throw Error(`--no-docker must not be used in CI/CD (detected ${r}=${n[r]}): it does not run WireMock in a container. Run the E2E suite with Docker instead.`);let i=new NoDockerRuntime({jar:resolveJar(e.jar??n.WIREMOCK_JAR),java:e.java??n.WIREMOCK_JAVA??(n.JAVA_HOME?(0,t.join)(n.JAVA_HOME,`bin`,`java`):`java`),startupTimeout:e.startupTimeout??6e4,cacheDir:e.cacheDir??(0,t.join)((0,u.tmpdir)(),`wiremock-testcontainers`),log:e.log??(e=>process.emitWarning(e,{code:`WIREMOCK_NO_DOCKER`}))});return setRuntime(i),()=>{getRuntime()===i&&setRuntime(void 0)}}function resolveJar(n){if(n){let r=(0,t.resolve)(n);if(!(0,e.existsSync)(r))throw Error(`--no-docker: WireMock JAR "${r}" does not exist`);return r}let r;try{r=(0,t.join)((0,t.dirname)((0,i.createRequire)((0,t.join)(process.cwd(),`noop.js`)).resolve(`wiremock/package.json`)),`build`)}catch{throw Error('--no-docker: no WireMock standalone JAR found – install the "wiremock" npm package (e.g. `npm i -D wiremock`) or set WIREMOCK_JAR to a wiremock-standalone-*.jar')}let a=(0,e.existsSync)(r)?(0,e.readdirSync)(r).find(e=>/^wiremock-standalone-.+\.jar$/u.test(e)):void 0;if(!a)throw Error(`--no-docker: the "wiremock" npm package contains no wiremock-standalone-*.jar in "${r}"`);return(0,t.join)(r,a)}function classData(n,r,i){if(n!==!1)try{(0,e.mkdirSync)(n,{recursive:!0});let{size:a,mtimeMs:o}=(0,e.statSync)(r),c=(0,s.createHash)(`sha256`).update(`${r}\0${a}\0${o}\0${i}`).digest(`hex`).slice(0,16),l=(0,t.join)(n,`wiremock-${c}.jsa`);if((0,e.existsSync)(l))return{path:l};for(let r of(0,e.readdirSync)(n)){let i=(0,t.join)(n,r);r.endsWith(`.tmp`)&&Date.now()-(0,e.statSync)(i).mtimeMs>36e5&&(0,e.rmSync)(i,{force:!0})}return{path:l,recording:`${l}.${(0,s.randomUUID)()}.tmp`}}catch{return}}var NoDockerRuntime=class{#e;#t;constructor(e){this.#e=e}get mainClass(){return this.#t??=mainClass((0,e.readFileSync)(this.#e.jar))??`wiremock.Run`,this.#t}async start(n){let r=(0,e.mkdtempSync)((0,t.join)((0,u.tmpdir)(),`wiremock-no-docker-`));try{await materialize(n,r);let i=(0,t.join)(r,`/home/wiremock`);for(let n of[`mappings`,`__files`])(0,e.mkdirSync)((0,t.join)(i,n),{recursive:!0});let a=new NoDockerContainer(n,this.#e,r,i,await freePort(),()=>this.mainClass);return await a.launch(),this.#e.log(a.describe()),a}catch(t){throw(0,e.rmSync)(r,{recursive:!0,force:!0}),t}}};async function materialize(n,r){let at=n=>{let i=(0,t.join)(r,n);return(0,e.mkdirSync)((0,t.dirname)(i),{recursive:!0}),i};for(let{source:t,target:r}of n.directories)(0,e.cpSync)(t,at(r),{recursive:!0});for(let{source:t,target:r}of n.files)(0,e.copyFileSync)(t,at(r));let i=await Promise.all(n.contents.map(async({content:e,target:t})=>({target:t,bytes:e instanceof d.Readable?await(0,f.buffer)(e):e})));for(let{target:t,bytes:n}of i)(0,e.writeFileSync)(at(t),n)}function freePort(){return new Promise((e,t)=>{let n=(0,l.createServer)();n.once(`error`,t),n.listen(0,w,()=>{let{port:t}=n.address();n.close(()=>e(t))})})}async function isHealthy(e){try{let t=await fetch(`${e}/__admin/health`);return t.status===404?(await fetch(`${e}/__admin/mappings`)).ok:t.ok}catch{return!1}}const split=e=>e?.split(/\s+/u).filter(Boolean)??[],E=/* @__PURE__ */ new Set;let D=!1;const killOnExit=e=>{E.add(e),D||(D=!0,process.once(`exit`,()=>{for(let e of E)e.kill(`SIGKILL`)}))},unsupported=e=>/* @__PURE__ */ Error(`${e}() is not supported with --no-docker: WireMock runs as a local JVM process, not in a container`);var NoDockerContainer=class{#e;#t;#n;#r;#i;#a=`no-docker-${(0,s.randomUUID)()}`;#o;#s;#c;#l;#u=``;constructor(e,t,n,r,i,a){this.#e=e,this.#t=t,this.#n=n,this.#r=r,this.#i=i,this.#o=a}#d(){let n=(0,t.join)(this.#n,m);return(0,e.existsSync)(n)&&(0,e.readdirSync)(n).some(e=>/\.jar$/iu.test(e))?[`-cp`,[this.#t.jar,(0,t.join)(n,`*`)].join(t.delimiter),this.#o()]:[`-jar`,this.#t.jar]}async launch(){let{environment:t,command:n}=this.#e,r=this.#d(),i=classData(this.#t.cacheDir,this.#t.jar,this.#t.java),a=this.#c=i?.recording&&r[0]===`-cp`?void 0:i,s=[...T,...a?[a.recording?`-XX:ArchiveClassesAtExit=${a.recording}`:`-XX:SharedArchiveFile=${a.path}`]:[],...split(t.JAVA_OPTS),...r,`--port`,String(this.#i),`--bind-address`,w,`--root-dir`,this.#r,...n,...split(t.WIREMOCK_OPTIONS)];this.#u=``,this.#l=void 0;let c=(0,o.spawn)(this.#t.java,s,{env:{...process.env,...t},stdio:[`ignore`,`pipe`,`pipe`]});this.#s=c,c.once(`error`,e=>{this.#l=e});let record=e=>{this.#u=(this.#u+e.toString()).slice(-65536)};c.stdout?.on(`data`,record),c.stderr?.on(`data`,record),killOnExit(c);try{await this.#f(c)}catch(e){throw await this.#p(),e}a&&!a.recording&&/\[warning\]\[cds/u.test(this.#u)&&(0,e.rmSync)(a.path,{force:!0})}async#f(e){let t=Date.now()+this.#t.startupTimeout,output=()=>this.#u.trim()?`\n--- WireMock output ---\n${this.#u.trim()}`:``,poll=async()=>{if(this.#l){let e=this.#l.code===`ENOENT`;throw Error(e?`--no-docker: Java executable "${this.#t.java}" not found – WireMock's standalone JAR needs a Java 11+ runtime; install one or set WIREMOCK_JAVA / JAVA_HOME`:`--no-docker: could not start "${this.#t.java}": ${this.#l.message}`,{cause:this.#l})}if(e.exitCode!==null||e.signalCode!==null)throw Error(`--no-docker: WireMock exited during start-up (${e.signalCode??`exit code ${e.exitCode}`})${output()}`);if(!await isHealthy(this.#m)){if(Date.now()>=t)throw Error(`--no-docker: WireMock was not healthy within ${this.#t.startupTimeout}ms${output()}`);return await new Promise(e=>setTimeout(e,25)),poll()}};await poll()}async#p(){let t=this.#s,n=this.#c?.recording;if(E.delete(t),t?.pid&&t.exitCode===null&&t.signalCode===null){let e=(0,c.once)(t,`exit`);t.kill(n?`SIGTERM`:`SIGKILL`);let r=setTimeout(()=>t.kill(`SIGKILL`),1e4);await e,clearTimeout(r)}if(n){try{t?.exitCode===143&&(0,e.existsSync)(n)&&(0,e.renameSync)(n,this.#c.path)}catch{}(0,e.rmSync)(n,{force:!0}),this.#c=void 0}}get#m(){return`http://${w}:${this.#i}`}describe(){let e=/wiremock-standalone-(.+)\.jar$/u.exec(this.#t.jar)?.[1]??`unknown`,{image:t}=this.#e,n=t.slice(t.lastIndexOf(`:`)+1),r=/^(\d+\.\d+\.\d+)/u.exec(n)?.[1],i=r&&r!==e?` The image tag "${n}" differs from the JAR version ${e}.`:``;return`--no-docker: "${this.#e.image}" is NOT running in a container – WireMock ${e} standalone (local JVM, pid ${this.#s?.pid}) serves ${this.#m} instead.${i} Local development only; never use --no-docker in CI/CD.`}async stop(){await this.#p(),(0,e.rmSync)(this.#n,{recursive:!0,force:!0});let t=this.#a;return{getId:()=>t,copyArchiveFromContainer:async()=>Promise.reject(unsupported(`copyArchiveFromContainer`))}}async restart(){await this.#p(),await this.launch()}async[Symbol.asyncDispose](){await this.stop()}getHost(){return w}getHostname(){return this.getName()}getFirstMappedPort(){return this.#i}getMappedPort(e,t=`tcp`){let n=typeof e==`number`?`${e}/${t}`:e;if(n!==`8080/tcp`)throw Error(`No port mapping found for "${n}"`);return this.#i}getName(){return this.#e.name??`wiremock-no-docker-${this.#i}`}getLabels(){return{}}getId(){return this.#a}getNetworkNames(){return[]}async logs(){return d.Readable.from([this.#u])}getNetworkId(){throw unsupported(`getNetworkId`)}getIpAddress(){throw unsupported(`getIpAddress`)}async commit(){throw unsupported(`commit`)}async exec(){throw unsupported(`exec`)}async copyArchiveFromContainer(){throw unsupported(`copyArchiveFromContainer`)}async copyArchiveToContainer(){throw unsupported(`copyArchiveToContainer`)}async copyDirectoriesToContainer(){throw unsupported(`copyDirectoriesToContainer`)}async copyFilesToContainer(){throw unsupported(`copyFilesToContainer`)}async copyContentToContainer(){throw unsupported(`copyContentToContainer`)}};const O=`/home/wiremock/mappings`,k=`/home/wiremock/__files`,A=`inline JSON`;var WiremockContainer=class extends n.GenericContainer{#e=/* @__PURE__ */ new Map;#t=/* @__PURE__ */ new Map;#n=/* @__PURE__ */ new Set;#r=/* @__PURE__ */ new Map;#i=/* @__PURE__ */ new Map;#a=[];#o={scanning:!0,verifyClassNames:!0};#s;constructor(e=`wiremock/wiremock`){super(e),this.withExposedPorts(8080),this.withWaitStrategy(n.Wait.forListeningPorts())}withCommand(e){return this.#s=e,super.withCommand(e)}withMappings(n){return within(`WiremockContainer.withMappings`,()=>{let r=(0,t.join)(n,`mappings`);(0,e.existsSync)(r)||fail(`"${r}" does not exist`),(0,e.statSync)(r).isDirectory()||fail(`"${r}" is not a directory`);let i=(0,t.join)(n,`__files`),a=[{source:r,target:O},...(0,e.existsSync)(i)?[{source:i,target:k}]:[]];this.#m(a.flatMap(({source:e,target:n})=>listFiles(e).map(r=>({target:t.posix.join(n,r),origin:(0,t.resolve)(e,r),ids:n===O?stubIdsOf((0,t.join)(e,r)):[]})))),this.withCopyDirectoriesToContainer(a)}),this}withMapping(n,r){return within(`WiremockContainer.withMapping("${n}")`,()=>{let i=containerName(n),a=t.posix.join(O,i.endsWith(`.json`)?i:`${i}.json`),o=isRecord(r)&&`json`in r,s=o?A:resolveFile(r),c=o?inlineJson(r.json):(0,e.readFileSync)(s),l=within(s,()=>stubIds(c.toString()));this.withCopyContentToContainer(this.#m([{target:a,origin:s,ids:l}]).map(()=>({content:c,target:a})))}),this}withFiles(...e){return within(`WiremockContainer.withFiles`,()=>{let n=e.map(e=>{let n=resolveFile(e),r=isRecord(e)&&e.name!==void 0?containerName(e.name):(0,t.basename)(n);return{target:t.posix.join(k,r),origin:n}});this.withCopyFilesToContainer(this.#m(n).map(({origin:e,target:t})=>({source:e,target:t})))}),this}withExtension(e){return this.#c(`withExtension`,[e])}withExtensions(e){return this.#c(`withExtensions`,e)}withExtensionJar(e){return this.#l(`withExtensionJar`,[e])}withExtensionJars(e){return this.#l(`withExtensionJars`,e)}withExtensionOptions(e){return within(`WiremockContainer.withExtensionOptions`,()=>{let t=Object.entries(isRecord(e)?e:fail(`expected an options object, got ${show(e)}`));for(let[e,n]of t)e in this.#o||fail(`unknown option "${e}"; expected ${Object.keys(this.#o).join(` or `)}`),n!==void 0&&typeof n!=`boolean`&&fail(`"${e}" must be a boolean, got ${show(n)}`);this.#o={...this.#o,...Object.fromEntries(t.filter(([,e])=>e!==void 0))}}),this}async start(){let e=within(`WiremockContainer.start`,()=>this.#d());this.createOpts.Cmd=e.length>0?[...this.#s??[],...e]:this.#s?.slice();let t=getRuntime();try{let e=t?await t.start(this.#p()):await super.start();return new StartedWiremockContainer(e,`http://${e.getHost()}:${e.getMappedPort(8080)}`)}catch(e){throw this.#f(e)}}#c(e,t){return within(`WiremockContainer.${e}`,()=>{let e=(Array.isArray(t)?t:fail(`expected an array, got ${show(t)}`)).map(contribution),n=new Map(this.#r),r=this.#a;for(let{name:t,classNames:i,jars:a,options:o}of e){if(t===void 0)continue;let e=JSON.stringify([i,a.map(({origin:e,target:t})=>[e,t]),o]);(n.get(t)??e)!==e&&fail(`another extension named "${t}" is already registered; names must be unique`),n.set(t,e),r=within(`extension "${t}"`,()=>mergeOptions(r,o,`another extension`))}this.#u(e.flatMap(({jars:e})=>e)),[this.#r,this.#a]=[n,r];for(let t of e.flatMap(({classNames:e})=>e))this.#n.add(t)}),this}#l(e,t){return within(`WiremockContainer.${e}`,()=>this.#u((Array.isArray(t)?t:fail(`expected an array, got ${show(t)}`)).flatMap(resolveJars))),this}#u(e){let t=this.#m(e);for(let e of t)this.#i.set(e.target,e);this.withCopyFilesToContainer(t.map(({origin:e,target:t})=>({source:e,target:t})))}#d(){let{scanning:e,verifyClassNames:t}=this.#o;t&&verifyClassNames(this.#n,[...this.#i.values()]);let n=[...this.#s??[],...this.environment.WIREMOCK_OPTIONS?.split(/\s+/u).filter(Boolean)??[]],r=mergeOptions(n,[...this.#a,...e?[]:[`--disable-extensions-scanning`]],`withCommand() or WIREMOCK_OPTIONS`).slice(n.length);if(this.#n.size===0)return r;let i=n.find(e=>optionName(e)===`--extensions`);return i&&fail(`"${i}" is already set by withCommand() or WIREMOCK_OPTIONS; WireMock accepts it only once – register those classes with withExtensions() instead`),[...r,`--extensions`,[...this.#n].join(`,`)]}#f(e){if(this.#n.size+this.#i.size===0)return e;let n=[...this.#i.keys()].map(e=>(0,t.basename)(e));return Error(`${e.message}\nWireMock was started with extensions: ${[...this.#n].join(`, `)||`none`}; JARs: ${n.join(`, `)||`none`}. An extension stops WireMock if its class is missing (ClassNotFoundException), lacks a public no-argument constructor or targets a newer Java than the image's; WireMock's output names the cause (Docker: run with DEBUG=testcontainers:containers).`,{cause:e})}#p(){return{image:this.imageName.string,name:this.createOpts.name,command:[...this.createOpts.Cmd??[]],environment:{...this.environment},directories:this.directoriesToCopy.map(({source:e,target:t})=>({source:e,target:t})),files:this.filesToCopy.map(({source:e,target:t})=>({source:e,target:t})),contents:this.contentsToCopy.map(({content:e,target:t})=>({content:e,target:t}))}}#m(e){let t=new Map(this.#e),n=new Map(this.#t),r=e.filter(({target:e,origin:r,ids:i=[]})=>{let a=t.get(e);if(a===r&&r!==A)return!1;a&&fail(`"${e}" is already registered from ${a}; registering ${r} would overwrite it`);for(let t of i){let i=n.get(t);i&&fail(`stub id "${t}" in ${r} is already used by "${i}"; WireMock refuses to start with duplicate stub ids`),n.set(t,e)}return t.set(e,r),!0});return[this.#e,this.#t]=[t,n],r}};const j=`/__admin`;var StartedWiremockContainer=class extends n.AbstractStartedContainer{#e;constructor(e,t){super(e),this.#e=t}get rpcUrl(){return this.#e}getUrl(e=``){if(e===``)return this.#e;let t=e.startsWith(`/`)||e.startsWith(`?`)?e:`/${e}`;return`${this.#e}${t}`}getAdminUrl(){return`${this.#e}${j}`}getMappingUrl(){return`${this.getAdminUrl()}/mappings`}async isRunning(){try{let e=await fetch(`${this.getAdminUrl()}/health`);return e.status===404?(await fetch(this.getMappingUrl())).ok:e.ok}catch{return!1}}async waitUntilReady(e={}){let{timeout:t=3e4,interval:n=250}=e,r=Date.now()+t,poll=async()=>{if(!await this.isRunning()){if(Date.now()>=r)throw Error(`WireMock at ${this.#e} was not ready within ${t}ms`);return await new Promise(e=>setTimeout(e,n)),poll()}};await poll()}async reset(){await this.#t(`POST`,`/reset`)}async resetRequests(){await this.#t(`DELETE`,`/requests`)}async countRequests(e){let{count:t}=await(await this.#t(`POST`,`/requests/count`,e)).json();return t}async findRequests(e){let{requests:t}=await(await this.#t(`POST`,`/requests/find`,e)).json();return t??[]}async#t(e,t,n){let r=await fetch(`${this.getAdminUrl()}${t}`,{method:e,...n===void 0?{}:{headers:{"Content-Type":`application/json`},body:JSON.stringify(n)}});if(!r.ok){let n=await r.text().catch(()=>``);throw Error(`WireMock admin request ${e} ${j}${t} failed with ${r.status} ${r.statusText}${n?`: ${n}`:``}`)}return r}};exports.NO_DOCKER_ENV=S,exports.StartedWiremockContainer=StartedWiremockContainer,exports.WiremockContainer=WiremockContainer,exports.detectCI=detectCI,exports.installNoDockerRuntime=installNoDockerRuntime,exports.isNoDockerRequested=isNoDockerRequested;
|
package/dist/index.d.cts
CHANGED
|
@@ -27,11 +27,82 @@ type WiremockMappingSource = WiremockFileSource | {
|
|
|
27
27
|
type WiremockStaticFile = WiremockPath | (Exclude<WiremockFileSource, WiremockPath> & {
|
|
28
28
|
readonly name?: string;
|
|
29
29
|
});
|
|
30
|
+
/** An extension JAR, or a directory of them – any {@link WiremockStaticFile}; `name` renames a JAR. */
|
|
31
|
+
type WiremockExtensionJar = WiremockStaticFile;
|
|
32
|
+
/**
|
|
33
|
+
* Base interface all WireMock extensions must implement: what WireMock loads for it. Implement at
|
|
34
|
+
* least one optional member.
|
|
35
|
+
* @example
|
|
36
|
+
* class Uppercase implements WireMockExtension {
|
|
37
|
+
* getName() { return "uppercase"; }
|
|
38
|
+
* getClassNames() { return ["com.acme.UppercaseTransformer"]; }
|
|
39
|
+
* getJars() { return ["./acme.jar"]; }
|
|
40
|
+
* }
|
|
41
|
+
*/
|
|
42
|
+
interface WireMockExtension {
|
|
43
|
+
/** Its name, e.g. WireMock's `"uppercase"`. Re-registering a name: no-op if configured alike, else an error. */
|
|
44
|
+
getName(): string;
|
|
45
|
+
/** Fully qualified Java class names, passed to `--extensions`. */
|
|
46
|
+
getClassNames?(): readonly string[];
|
|
47
|
+
/** JARs – or directories of them – for WireMock's class path. */
|
|
48
|
+
getJars?(): readonly WiremockExtensionJar[];
|
|
49
|
+
/** WireMock CLI options, one per entry: `"--flag"` or `"--option=value"`. */
|
|
50
|
+
getOptions?(): readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/** Represents a class constructor for an extension */
|
|
53
|
+
type ExtensionConstructor<T extends WireMockExtension = WireMockExtension> = new () => T;
|
|
54
|
+
/** Union type of all supported ways to register an extension: a Java class name, a {@link WireMockExtension} or its class. */
|
|
55
|
+
type ExtensionInput = string | ExtensionConstructor | WireMockExtension;
|
|
56
|
+
/** Options for `WiremockContainer.withExtensionOptions()`. */
|
|
57
|
+
interface WiremockExtensionOptions {
|
|
58
|
+
/** Load extensions JARs declare in `META-INF/services`; `false`: `--disable-extensions-scanning`. Default `true`. */
|
|
59
|
+
readonly scanning?: boolean;
|
|
60
|
+
/** Fail `start()` for a class in no registered JAR and not WireMock's own; off for classes the image provides. Default `true`. */
|
|
61
|
+
readonly verifyClassNames?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** Set to `1` by the `--no-docker` flag. */
|
|
64
|
+
export declare const NO_DOCKER_ENV = "WIREMOCK_NO_DOCKER";
|
|
65
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
66
|
+
/** Options for {@link installNoDockerRuntime}. */
|
|
67
|
+
interface NoDockerOptions {
|
|
68
|
+
/** WireMock standalone JAR. Default: `$WIREMOCK_JAR`, else the `wiremock` npm package's. */
|
|
69
|
+
readonly jar?: string;
|
|
70
|
+
/** Java executable. Default: `$WIREMOCK_JAVA`, `$JAVA_HOME/bin/java`, else `java`. */
|
|
71
|
+
readonly java?: string;
|
|
72
|
+
/** Milliseconds to wait for WireMock to be healthy. Default `60_000`. */
|
|
73
|
+
readonly startupTimeout?: number;
|
|
74
|
+
/** Receives the per-start warning. Default: `process.emitWarning`. */
|
|
75
|
+
readonly log?: (message: string) => void;
|
|
76
|
+
/**
|
|
77
|
+
* Cache for the class-data archive that halves JVM start-up after the first run; `false`
|
|
78
|
+
* disables it. Default: `<os tmpdir>/wiremock-testcontainers`.
|
|
79
|
+
*/
|
|
80
|
+
readonly cacheDir?: string | false;
|
|
81
|
+
/** Source of the CI/CD check and the defaults above. Default: `process.env`. */
|
|
82
|
+
readonly env?: Environment;
|
|
83
|
+
}
|
|
84
|
+
/** The CI/CD variable set in `env`, if any. */
|
|
85
|
+
export declare function detectCI(env?: Environment): string | undefined;
|
|
86
|
+
/** Whether {@link NO_DOCKER_ENV} is `1` or `true`. */
|
|
87
|
+
export declare function isNoDockerRequested(env?: Environment): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* **Local development only**: serves every `WiremockContainer` started from now on from a local
|
|
90
|
+
* WireMock JVM (Java 11+) on `127.0.0.1`, warning on each start. Container-only features throw.
|
|
91
|
+
* @example
|
|
92
|
+
* // setup file, registered only when WIREMOCK_NO_DOCKER=1
|
|
93
|
+
* import { installNoDockerRuntime } from "@hellaweb3/wiremock-testcontainers-nodejs/no-docker";
|
|
94
|
+
* installNoDockerRuntime();
|
|
95
|
+
* @returns A function that uninstalls it.
|
|
96
|
+
* @throws In CI/CD, or if no WireMock JAR is found.
|
|
97
|
+
*/
|
|
98
|
+
export declare function installNoDockerRuntime(options?: NoDockerOptions): () => void;
|
|
30
99
|
/** A WireMock Testcontainer. Registrations are validated at once and may not overwrite one another. */
|
|
31
100
|
export declare class WiremockContainer extends GenericContainer {
|
|
32
101
|
#private;
|
|
33
102
|
/** @param image Docker image, default `wiremock/wiremock`. */
|
|
34
103
|
constructor(image?: string);
|
|
104
|
+
/** WireMock CLI arguments, e.g. `["--verbose"]`; extensions add theirs on `start()`. */
|
|
105
|
+
override withCommand(command: string[]): this;
|
|
35
106
|
/**
|
|
36
107
|
* Copies `directory/mappings/` (required) and `directory/__files/` (optional) into the container.
|
|
37
108
|
* @throws If `mappings/` is missing or a file would overwrite an earlier registration.
|
|
@@ -48,7 +119,34 @@ export declare class WiremockContainer extends GenericContainer {
|
|
|
48
119
|
* @throws If a source or name is invalid or a file would overwrite an earlier registration.
|
|
49
120
|
*/
|
|
50
121
|
withFiles(...files: WiremockStaticFile[]): this;
|
|
51
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Registers an extension: a fully qualified Java class name, or a {@link WireMockExtension} or its
|
|
124
|
+
* class. Class names share one `--extensions`, in registration order, without repeats.
|
|
125
|
+
* @example .withExtensionJar("./acme.jar").withExtension("com.acme.UppercaseTransformer")
|
|
126
|
+
* @throws If a class name, an extension or its JARs are invalid, or an option conflicts.
|
|
127
|
+
*/
|
|
128
|
+
withExtension(extension: ExtensionInput): this;
|
|
129
|
+
/** {@link withExtension} for several extensions – all or nothing. */
|
|
130
|
+
withExtensions(extensions: readonly ExtensionInput[]): this;
|
|
131
|
+
/**
|
|
132
|
+
* Puts a JAR – or every `*.jar` of a directory – on WireMock's class path. Extensions listed in
|
|
133
|
+
* its `META-INF/services` load by themselves; others need {@link withExtension}.
|
|
134
|
+
* @example .withExtensionJar("./acme.jar").withExtensionJar({ package: "@acme/wiremock", file: "jars" })
|
|
135
|
+
* @throws If the source is missing, unreadable or not a JAR, or another JAR has the same name.
|
|
136
|
+
*/
|
|
137
|
+
withExtensionJar(jar: WiremockExtensionJar): this;
|
|
138
|
+
/** {@link withExtensionJar} for several JARs or directories – all or nothing. */
|
|
139
|
+
withExtensionJars(jars: readonly WiremockExtensionJar[]): this;
|
|
140
|
+
/**
|
|
141
|
+
* Configures how extensions load; options not given keep their value.
|
|
142
|
+
* @throws For an unknown option or a non-boolean value.
|
|
143
|
+
*/
|
|
144
|
+
withExtensionOptions(options: WiremockExtensionOptions): this;
|
|
145
|
+
/**
|
|
146
|
+
* Starts WireMock – in Docker, or locally once `./no-docker` is installed.
|
|
147
|
+
* @throws Before starting, for a class in no registered JAR or options conflicting with
|
|
148
|
+
* `withCommand()`; if WireMock fails, naming the extensions in use.
|
|
149
|
+
*/
|
|
52
150
|
override start(): Promise<StartedWiremockContainer>;
|
|
53
151
|
}
|
|
54
152
|
/** WireMock [request-matching](https://wiremock.org/docs/request-matching/) criteria; unknown fields pass through. */
|
|
@@ -121,4 +219,4 @@ export declare class StartedWiremockContainer extends AbstractStartedContainer {
|
|
|
121
219
|
/** Journal entries matching `request` since the last reset. */
|
|
122
220
|
findRequests(request: WiremockRequestCriteria): Promise<RecordedRequest[]>;
|
|
123
221
|
}
|
|
124
|
-
export type { WiremockFileSource, WiremockMappingSource, WiremockPath, WiremockStaticFile, WiremockStubMapping };
|
|
222
|
+
export type { ExtensionConstructor, ExtensionInput, NoDockerOptions, WireMockExtension, WiremockExtensionJar, WiremockExtensionOptions, WiremockFileSource, WiremockMappingSource, WiremockPath, WiremockStaticFile, WiremockStubMapping };
|
package/dist/index.d.ts
CHANGED
|
@@ -27,11 +27,82 @@ type WiremockMappingSource = WiremockFileSource | {
|
|
|
27
27
|
type WiremockStaticFile = WiremockPath | (Exclude<WiremockFileSource, WiremockPath> & {
|
|
28
28
|
readonly name?: string;
|
|
29
29
|
});
|
|
30
|
+
/** An extension JAR, or a directory of them – any {@link WiremockStaticFile}; `name` renames a JAR. */
|
|
31
|
+
type WiremockExtensionJar = WiremockStaticFile;
|
|
32
|
+
/**
|
|
33
|
+
* Base interface all WireMock extensions must implement: what WireMock loads for it. Implement at
|
|
34
|
+
* least one optional member.
|
|
35
|
+
* @example
|
|
36
|
+
* class Uppercase implements WireMockExtension {
|
|
37
|
+
* getName() { return "uppercase"; }
|
|
38
|
+
* getClassNames() { return ["com.acme.UppercaseTransformer"]; }
|
|
39
|
+
* getJars() { return ["./acme.jar"]; }
|
|
40
|
+
* }
|
|
41
|
+
*/
|
|
42
|
+
interface WireMockExtension {
|
|
43
|
+
/** Its name, e.g. WireMock's `"uppercase"`. Re-registering a name: no-op if configured alike, else an error. */
|
|
44
|
+
getName(): string;
|
|
45
|
+
/** Fully qualified Java class names, passed to `--extensions`. */
|
|
46
|
+
getClassNames?(): readonly string[];
|
|
47
|
+
/** JARs – or directories of them – for WireMock's class path. */
|
|
48
|
+
getJars?(): readonly WiremockExtensionJar[];
|
|
49
|
+
/** WireMock CLI options, one per entry: `"--flag"` or `"--option=value"`. */
|
|
50
|
+
getOptions?(): readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/** Represents a class constructor for an extension */
|
|
53
|
+
type ExtensionConstructor<T extends WireMockExtension = WireMockExtension> = new () => T;
|
|
54
|
+
/** Union type of all supported ways to register an extension: a Java class name, a {@link WireMockExtension} or its class. */
|
|
55
|
+
type ExtensionInput = string | ExtensionConstructor | WireMockExtension;
|
|
56
|
+
/** Options for `WiremockContainer.withExtensionOptions()`. */
|
|
57
|
+
interface WiremockExtensionOptions {
|
|
58
|
+
/** Load extensions JARs declare in `META-INF/services`; `false`: `--disable-extensions-scanning`. Default `true`. */
|
|
59
|
+
readonly scanning?: boolean;
|
|
60
|
+
/** Fail `start()` for a class in no registered JAR and not WireMock's own; off for classes the image provides. Default `true`. */
|
|
61
|
+
readonly verifyClassNames?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** Set to `1` by the `--no-docker` flag. */
|
|
64
|
+
export declare const NO_DOCKER_ENV = "WIREMOCK_NO_DOCKER";
|
|
65
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
66
|
+
/** Options for {@link installNoDockerRuntime}. */
|
|
67
|
+
interface NoDockerOptions {
|
|
68
|
+
/** WireMock standalone JAR. Default: `$WIREMOCK_JAR`, else the `wiremock` npm package's. */
|
|
69
|
+
readonly jar?: string;
|
|
70
|
+
/** Java executable. Default: `$WIREMOCK_JAVA`, `$JAVA_HOME/bin/java`, else `java`. */
|
|
71
|
+
readonly java?: string;
|
|
72
|
+
/** Milliseconds to wait for WireMock to be healthy. Default `60_000`. */
|
|
73
|
+
readonly startupTimeout?: number;
|
|
74
|
+
/** Receives the per-start warning. Default: `process.emitWarning`. */
|
|
75
|
+
readonly log?: (message: string) => void;
|
|
76
|
+
/**
|
|
77
|
+
* Cache for the class-data archive that halves JVM start-up after the first run; `false`
|
|
78
|
+
* disables it. Default: `<os tmpdir>/wiremock-testcontainers`.
|
|
79
|
+
*/
|
|
80
|
+
readonly cacheDir?: string | false;
|
|
81
|
+
/** Source of the CI/CD check and the defaults above. Default: `process.env`. */
|
|
82
|
+
readonly env?: Environment;
|
|
83
|
+
}
|
|
84
|
+
/** The CI/CD variable set in `env`, if any. */
|
|
85
|
+
export declare function detectCI(env?: Environment): string | undefined;
|
|
86
|
+
/** Whether {@link NO_DOCKER_ENV} is `1` or `true`. */
|
|
87
|
+
export declare function isNoDockerRequested(env?: Environment): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* **Local development only**: serves every `WiremockContainer` started from now on from a local
|
|
90
|
+
* WireMock JVM (Java 11+) on `127.0.0.1`, warning on each start. Container-only features throw.
|
|
91
|
+
* @example
|
|
92
|
+
* // setup file, registered only when WIREMOCK_NO_DOCKER=1
|
|
93
|
+
* import { installNoDockerRuntime } from "@hellaweb3/wiremock-testcontainers-nodejs/no-docker";
|
|
94
|
+
* installNoDockerRuntime();
|
|
95
|
+
* @returns A function that uninstalls it.
|
|
96
|
+
* @throws In CI/CD, or if no WireMock JAR is found.
|
|
97
|
+
*/
|
|
98
|
+
export declare function installNoDockerRuntime(options?: NoDockerOptions): () => void;
|
|
30
99
|
/** A WireMock Testcontainer. Registrations are validated at once and may not overwrite one another. */
|
|
31
100
|
export declare class WiremockContainer extends GenericContainer {
|
|
32
101
|
#private;
|
|
33
102
|
/** @param image Docker image, default `wiremock/wiremock`. */
|
|
34
103
|
constructor(image?: string);
|
|
104
|
+
/** WireMock CLI arguments, e.g. `["--verbose"]`; extensions add theirs on `start()`. */
|
|
105
|
+
override withCommand(command: string[]): this;
|
|
35
106
|
/**
|
|
36
107
|
* Copies `directory/mappings/` (required) and `directory/__files/` (optional) into the container.
|
|
37
108
|
* @throws If `mappings/` is missing or a file would overwrite an earlier registration.
|
|
@@ -48,7 +119,34 @@ export declare class WiremockContainer extends GenericContainer {
|
|
|
48
119
|
* @throws If a source or name is invalid or a file would overwrite an earlier registration.
|
|
49
120
|
*/
|
|
50
121
|
withFiles(...files: WiremockStaticFile[]): this;
|
|
51
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Registers an extension: a fully qualified Java class name, or a {@link WireMockExtension} or its
|
|
124
|
+
* class. Class names share one `--extensions`, in registration order, without repeats.
|
|
125
|
+
* @example .withExtensionJar("./acme.jar").withExtension("com.acme.UppercaseTransformer")
|
|
126
|
+
* @throws If a class name, an extension or its JARs are invalid, or an option conflicts.
|
|
127
|
+
*/
|
|
128
|
+
withExtension(extension: ExtensionInput): this;
|
|
129
|
+
/** {@link withExtension} for several extensions – all or nothing. */
|
|
130
|
+
withExtensions(extensions: readonly ExtensionInput[]): this;
|
|
131
|
+
/**
|
|
132
|
+
* Puts a JAR – or every `*.jar` of a directory – on WireMock's class path. Extensions listed in
|
|
133
|
+
* its `META-INF/services` load by themselves; others need {@link withExtension}.
|
|
134
|
+
* @example .withExtensionJar("./acme.jar").withExtensionJar({ package: "@acme/wiremock", file: "jars" })
|
|
135
|
+
* @throws If the source is missing, unreadable or not a JAR, or another JAR has the same name.
|
|
136
|
+
*/
|
|
137
|
+
withExtensionJar(jar: WiremockExtensionJar): this;
|
|
138
|
+
/** {@link withExtensionJar} for several JARs or directories – all or nothing. */
|
|
139
|
+
withExtensionJars(jars: readonly WiremockExtensionJar[]): this;
|
|
140
|
+
/**
|
|
141
|
+
* Configures how extensions load; options not given keep their value.
|
|
142
|
+
* @throws For an unknown option or a non-boolean value.
|
|
143
|
+
*/
|
|
144
|
+
withExtensionOptions(options: WiremockExtensionOptions): this;
|
|
145
|
+
/**
|
|
146
|
+
* Starts WireMock – in Docker, or locally once `./no-docker` is installed.
|
|
147
|
+
* @throws Before starting, for a class in no registered JAR or options conflicting with
|
|
148
|
+
* `withCommand()`; if WireMock fails, naming the extensions in use.
|
|
149
|
+
*/
|
|
52
150
|
override start(): Promise<StartedWiremockContainer>;
|
|
53
151
|
}
|
|
54
152
|
/** WireMock [request-matching](https://wiremock.org/docs/request-matching/) criteria; unknown fields pass through. */
|
|
@@ -121,4 +219,4 @@ export declare class StartedWiremockContainer extends AbstractStartedContainer {
|
|
|
121
219
|
/** Journal entries matching `request` since the last reset. */
|
|
122
220
|
findRequests(request: WiremockRequestCriteria): Promise<RecordedRequest[]>;
|
|
123
221
|
}
|
|
124
|
-
export type { WiremockFileSource, WiremockMappingSource, WiremockPath, WiremockStaticFile, WiremockStubMapping };
|
|
222
|
+
export type { ExtensionConstructor, ExtensionInput, NoDockerOptions, WireMockExtension, WiremockExtensionJar, WiremockExtensionOptions, WiremockFileSource, WiremockMappingSource, WiremockPath, WiremockStaticFile, WiremockStubMapping };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as getRuntime}from"./runtime-ChyDTLYb.js";import{createRequire}from"node:module";import{existsSync,readFileSync,readdirSync,statSync}from"node:fs";import{basename,dirname,join,posix,relative,resolve,sep}from"node:path";import{AbstractStartedContainer,GenericContainer,Wait}from"testcontainers";import{fileURLToPath}from"node:url";const PACKAGE_NAME=/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u;function fail(message){throw new Error(message)}function within(context,fn){try{return fn()}catch(error){throw new Error(`${context}: ${error.message}`,{cause:error})}}const isRecord=value=>typeof value===`object`&&value!==null&&!Array.isArray(value);const show=value=>value instanceof URL?value.href:JSON.stringify(value)??String(value);const text=(value,field)=>typeof value===`string`&&value.trim()!==``?value:fail(`"${field}" must be a non-empty string, got ${show(value)}`);const toPath=(value,field)=>{if(!(value instanceof URL)&&!text(value,field).startsWith(`file:`))return resolve(value);try{return fileURLToPath(value)}catch{return fail(`"${field}" must be a path or a file: URL, got ${show(value)}`)}};const existingFile=path=>{const stats=statSync(path,{throwIfNoEntry:false});return stats?.isFile()?path:fail(`"${path}" ${stats?`is not a file`:`does not exist`}`)};const packageRoot=name=>{if(!PACKAGE_NAME.test(name))fail(`"${name}" is not a valid npm package name`);return(createRequire(join(process.cwd(),`noop.js`)).resolve.paths(name)??[]).map(dir=>join(dir,name)).find(dir=>existsSync(join(dir,`package.json`)))??fail(`package "${name}" is not installed (searched from "${process.cwd()}")`)};function resolveFile(source){if(typeof source===`string`||source instanceof URL)return existingFile(toPath(source,`source`));if(!isRecord(source))return fail(`unsupported source ${show(source)}; expected a path, a file: URL, { file }, { package, file } or { module, file }`);const{file,package:pkg,module}=source;if(pkg!==void 0&&module!==void 0)fail(`a source may set "package" or "module", not both`);if(pkg!==void 0){const root=packageRoot(text(pkg,`package`));const path=resolve(root,text(file,`file`));return relative(root,path).split(sep)[0]===`..`?fail(`"${String(file)}" must point to a file inside package "${pkg}"`):existingFile(path)}if(module===void 0)return existingFile(toPath(file,`file`));const anchor=toPath(module,`module`);return existingFile(resolve(statSync(anchor,{throwIfNoEntry:false})?.isDirectory()?anchor:dirname(anchor),text(file,`file`)))}const containerName=name=>{const value=text(name,`name`);return value.split(`/`).some(part=>[``,`.`,`..`].includes(part)||/[\\\0]/u.test(part))?fail(`name "${value}" must be a relative path using "/" separators, without empty, "." or ".." segments`):value};const inlineJson=json=>typeof json===`string`?json:JSON.stringify(json,null,2)??fail(`"json" must be a JSON string or an object`);const parseJson=json=>{try{return JSON.parse(json)}catch(error){return fail(`invalid JSON (${error.message})`)}};function stubIds(json){const value=parseJson(json);const many=isRecord(value)&&`mappings`in value;const stubs=many?value.mappings:[value];if(!Array.isArray(stubs)||stubs.length===0)fail(`"mappings" must be a non-empty array of stub mappings`);return stubs.flatMap((stub,index)=>{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)`);const id=stub.id??stub.uuid;return typeof id===`string`?[id]:[]})}const stubIdsOf=path=>{try{return path.endsWith(`.json`)?stubIds(readFileSync(path,`utf8`)):[]}catch{return[]}};const listFiles=dir=>readdirSync(dir,{recursive:true,withFileTypes:true}).filter(entry=>!entry.isDirectory()).map(entry=>relative(dir,join(entry.parentPath,entry.name)).split(sep).join(`/`));const MAPPINGS=`/home/wiremock/mappings`;const FILES=`/home/wiremock/__files`;const INLINE=`inline JSON`;var WiremockContainer=class extends GenericContainer{#origins=/* @__PURE__ */ new Map;#stubIds=/* @__PURE__ */ new Map;constructor(image=`wiremock/wiremock`){super(image);this.withExposedPorts(8080);this.withWaitStrategy(Wait.forListeningPorts())}withMappings(directory){within(`WiremockContainer.withMappings`,()=>{const mappings=join(directory,`mappings`);if(!existsSync(mappings))fail(`"${mappings}" does not exist`);if(!statSync(mappings).isDirectory())fail(`"${mappings}" is not a directory`);const files=join(directory,`__files`);const dirs=[{source:mappings,target:MAPPINGS},...existsSync(files)?[{source:files,target:FILES}]:[]];this.#claim(dirs.flatMap(({source,target})=>listFiles(source).map(name=>({target:posix.join(target,name),origin:resolve(source,name),ids:target===MAPPINGS?stubIdsOf(join(source,name)):[]}))));this.withCopyDirectoriesToContainer(dirs)});return this}withMapping(name,source){within(`WiremockContainer.withMapping("${name}")`,()=>{const file=containerName(name);const target=posix.join(MAPPINGS,file.endsWith(`.json`)?file:`${file}.json`);const inline=isRecord(source)&&`json`in source;const origin=inline?INLINE:resolveFile(source);const content=inline?inlineJson(source.json):readFileSync(origin);const ids=within(origin,()=>stubIds(content.toString()));this.withCopyContentToContainer(this.#claim([{target,origin,ids}]).map(()=>({content,target})))});return this}withFiles(...files){within(`WiremockContainer.withFiles`,()=>{const claims=files.map(file=>{const origin=resolveFile(file);const name=isRecord(file)&&file.name!==void 0?containerName(file.name):basename(origin);return{target:posix.join(FILES,name),origin}});this.withCopyFilesToContainer(this.#claim(claims).map(({origin,target})=>({source:origin,target})))});return this}async start(){const runtime=getRuntime();const startedContainer=runtime?await runtime.start(this.#startRequest()):await super.start();return new StartedWiremockContainer(startedContainer,`http://${startedContainer.getHost()}:${startedContainer.getMappedPort(8080)}`)}#startRequest(){return{image:this.imageName.string,name:this.createOpts.name,command:[...this.createOpts.Cmd??[]],environment:{...this.environment},directories:this.directoriesToCopy.map(({source,target})=>({source,target})),files:this.filesToCopy.map(({source,target})=>({source,target})),contents:this.contentsToCopy.map(({content,target})=>({content,target}))}}#claim(claims){const origins=new Map(this.#origins);const owners=new Map(this.#stubIds);const fresh=claims.filter(({target,origin,ids=[]})=>{const existing=origins.get(target);if(existing===origin&&origin!==INLINE)return false;if(existing)fail(`"${target}" is already registered from ${existing}; registering ${origin} would overwrite it`);for(const id of ids){const owner=owners.get(id);if(owner)fail(`stub id "${id}" in ${origin} is already used by "${owner}"; WireMock refuses to start with duplicate stub ids`);owners.set(id,target)}origins.set(target,origin);return true});[this.#origins,this.#stubIds]=[origins,owners];return fresh}};const ADMIN_PATH=`/__admin`;var StartedWiremockContainer=class extends AbstractStartedContainer{#rpcUrl;constructor(startedTestContainer,url){super(startedTestContainer);this.#rpcUrl=url}get rpcUrl(){return this.#rpcUrl}getUrl(path=``){if(path===``)return this.#rpcUrl;const suffix=path.startsWith(`/`)||path.startsWith(`?`)?path:`/${path}`;return`${this.#rpcUrl}${suffix}`}getAdminUrl(){return`${this.#rpcUrl}${ADMIN_PATH}`}getMappingUrl(){return`${this.getAdminUrl()}/mappings`}async isRunning(){try{const health=await fetch(`${this.getAdminUrl()}/health`);if(health.status!==404)return health.ok;return(await fetch(this.getMappingUrl())).ok}catch{return false}}async waitUntilReady(options={}){const{timeout=3e4,interval=250}=options;const deadline=Date.now()+timeout;const poll=async()=>{if(await this.isRunning())return;if(Date.now()>=deadline)throw new Error(`WireMock at ${this.#rpcUrl} was not ready within ${timeout}ms`);await new Promise(_resolve=>setTimeout(_resolve,interval));return poll()};await poll()}async reset(){await this.#admin(`POST`,`/reset`)}async resetRequests(){await this.#admin(`DELETE`,`/requests`)}async countRequests(request){const{count}=await(await this.#admin(`POST`,`/requests/count`,request)).json();return count}async findRequests(request){const{requests}=await(await this.#admin(`POST`,`/requests/find`,request)).json();return requests??[]}async#admin(method,path,body){const response=await fetch(`${this.getAdminUrl()}${path}`,{method,...body===void 0?{}:{headers:{"Content-Type":`application/json`},body:JSON.stringify(body)}});if(!response.ok){const detail=await response.text().catch(()=>``);throw new Error(`WireMock admin request ${method} ${ADMIN_PATH}${path} failed with ${response.status} ${response.statusText}${detail?`: ${detail}`:``}`)}return response}};export{StartedWiremockContainer,WiremockContainer};
|
|
1
|
+
import{createRequire as e}from"node:module";import{copyFileSync as t,cpSync as n,existsSync as r,mkdirSync as i,mkdtempSync as a,readFileSync as o,readdirSync as s,renameSync as c,rmSync as l,statSync as u,writeFileSync as d}from"node:fs";import{basename as f,delimiter as p,dirname as m,join as h,posix as g,relative as _,resolve as v,sep as y}from"node:path";import{AbstractStartedContainer as b,GenericContainer as x,Wait as S}from"testcontainers";import{inflateRawSync as C}from"node:zlib";import{fileURLToPath as w}from"node:url";import{spawn as T}from"node:child_process";import{createHash as E,randomUUID as D}from"node:crypto";import{once as O}from"node:events";import{createServer as k}from"node:net";import{tmpdir as A}from"node:os";import{Readable as j}from"node:stream";import{buffer as M}from"node:stream/consumers";const N=Buffer.from(`PK`,`latin1`);function jarEntries(e){let t=e.length<22?-1:e.lastIndexOf(N,e.length-22);if(t<0||t<e.length-22-65535)throw Error(`not a ZIP archive`);let n=e.readUInt16LE(t+10),r=e.readUInt32LE(t+16),i=t-20;if(i>=0&&e.readUInt32LE(i)===117853008){let t=Number(e.readBigUInt64LE(i+8));n=Number(e.readBigUInt64LE(t+32)),r=Number(e.readBigUInt64LE(t+48))}let a=[];for(let t=0;t<n;t++){if(r+46>e.length||e.readUInt32LE(r)!==33639248)throw Error(`corrupt ZIP central directory`);let t=e.readUInt16LE(r+28),n=e.toString(`utf8`,r+46,r+46+t);a.push({name:n,method:e.readUInt16LE(r+10),size:e.readUInt32LE(r+20),offset:e.readUInt32LE(r+42)}),r+=46+t+e.readUInt16LE(r+30)+e.readUInt16LE(r+32)}return a}function jarEntry(e,t){try{let n=jarEntries(e).find(e=>e.name===t);if(!n||e.readUInt32LE(n.offset)!==67324752)return;let r=n.offset+30+e.readUInt16LE(n.offset+26)+e.readUInt16LE(n.offset+28),i=e.subarray(r,r+n.size);return n.method===0?i:n.method===8?C(i):void 0}catch{return}}function mainClass(e){let t=jarEntry(e,`META-INF/MANIFEST.MF`)?.toString(`utf8`);return t&&/^Main-Class:\s*(\S+)\s*$/mu.exec(t)?.[1]}const P=`/var/wiremock/extensions`,F=Symbol.for(`@hellaweb3/wiremock-testcontainers-nodejs/runtime`),I=globalThis,getRuntime=()=>I[F],setRuntime=e=>{e?I[F]=e:delete I[F]},L=/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u;function fail(e){throw Error(e)}function within(e,t){try{return t()}catch(t){throw Error(`${e}: ${t.message}`,{cause:t})}}const isRecord=e=>typeof e==`object`&&!!e&&!Array.isArray(e),show=e=>e instanceof URL?e.href:JSON.stringify(e)??String(e),text=(e,t)=>typeof e==`string`&&e.trim()!==``?e:fail(`"${t}" must be a non-empty string, got ${show(e)}`),toPath=(e,t)=>{if(!(e instanceof URL)&&!text(e,t).startsWith(`file:`))return v(e);try{return w(e)}catch{return fail(`"${t}" must be a path or a file: URL, got ${show(e)}`)}},existingFile=e=>{let t=u(e,{throwIfNoEntry:!1});return t?.isFile()?e:fail(`"${e}" ${t?`is not a file`:`does not exist`}`)},packageRoot=t=>(L.test(t)||fail(`"${t}" is not a valid npm package name`),(e(h(process.cwd(),`noop.js`)).resolve.paths(t)??[]).map(e=>h(e,t)).find(e=>r(h(e,`package.json`)))??fail(`package "${t}" is not installed (searched from "${process.cwd()}")`));function resolvePath(e){if(typeof e==`string`||e instanceof URL)return toPath(e,`source`);if(!isRecord(e))return fail(`unsupported source ${show(e)}; expected a path, a file: URL, { file }, { package, file } or { module, file }`);let{file:t,package:n,module:r}=e;if(n!==void 0&&r!==void 0&&fail(`a source may set "package" or "module", not both`),n!==void 0){let e=packageRoot(text(n,`package`)),r=v(e,text(t,`file`));return _(e,r).split(y)[0]===`..`?fail(`"${String(t)}" must point to a file inside package "${n}"`):r}if(r===void 0)return toPath(t,`file`);let i=toPath(r,`module`);return v(u(i,{throwIfNoEntry:!1})?.isDirectory()?i:m(i),text(t,`file`))}const resolveFile=e=>existingFile(resolvePath(e)),containerName=e=>{let t=text(e,`name`);return t.split(`/`).some(e=>[``,`.`,`..`].includes(e)||/[\\\0]/u.test(e))?fail(`name "${t}" must be a relative path using "/" separators, without empty, "." or ".." segments`):t},inlineJson=e=>typeof e==`string`?e:JSON.stringify(e,null,2)??fail(`"json" must be a JSON string or an object`),parseJson=e=>{try{return JSON.parse(e)}catch(e){return fail(`invalid JSON (${e.message})`)}};function stubIds(e){let t=parseJson(e),n=isRecord(t)&&`mappings`in t,r=n?t.mappings:[t];return Array.isArray(r)&&r.length!==0||fail(`"mappings" must be a non-empty array of stub mappings`),r.flatMap((e,t)=>{isRecord(e)&&isRecord(e.request)&&isRecord(e.response)||fail(`${n?`mappings[${t}]`:`the mapping`} is not a WireMock stub mapping ("request" and "response" objects are required)`);let r=e.id??e.uuid;return typeof r==`string`?[r]:[]})}const stubIdsOf=e=>{try{return e.endsWith(`.json`)?stubIds(o(e,`utf8`)):[]}catch{return[]}},listFiles=e=>s(e,{recursive:!0,withFileTypes:!0}).filter(e=>!e.isDirectory()).map(t=>_(e,h(t.parentPath,t.name)).split(y).join(`/`)),R=String.raw`[\p{L}_$][\p{L}\p{N}_$]*`,z=RegExp(`^${R}(?:\\.${R})+$`,`u`),B=/^--[a-z\d][\w-]*(?:=.*)?$/iu,V=[`com.github.tomakehurst.wiremock.`,`org.wiremock.`];function className(e){let t=text(e,`class name`).trim();return z.test(t)&&!t.endsWith(`.class`)?t:fail(`"${t}" is not a fully qualified Java class name such as "com.acme.MyTransformer" (${t.endsWith(`.class`)?`drop the ".class" suffix`:/[/\\]/u.test(t)?`separate packages with "." not "/"`:t.includes(`.`)?`only letters, digits, _ and $ may form each part`:`include the package – an extension's short name is not enough`})`)}function inspectJar(e,t){t.includes(`/`)&&fail(`name "${t}" must be a file name: WireMock's class path holds only the JARs directly in ${P}`),/\.jar$/iu.test(t)||fail(`"${t}" must end in ".jar" – WireMock's class path picks up only *.jar files; pass { file, name: "<name>.jar" } to rename it`);let n;try{n=o(e)}catch(t){return fail(`"${e}" cannot be read: ${t.message}`)}let r=within(`"${e}" is not a valid JAR`,()=>jarEntries(n)).map(e=>e.name),i=r.filter(e=>e.endsWith(`.class`)&&!e.startsWith(`META-INF/`)).map(e=>e.slice(0,-6).replaceAll(`/`,`.`));return i.length===0&&!r.some(e=>e.startsWith(`META-INF/services/`))&&fail(`"${e}" contains no Java classes – is it really an extension JAR?`),{target:g.join(P,t),origin:e,classes:i}}function resolveJars(e){let t=resolvePath(e),n=u(t,{throwIfNoEntry:!1})??fail(`"${t}" does not exist`),r=isRecord(e)?e.name:void 0;if(n.isDirectory()){r!==void 0&&fail(`"name" cannot rename the directory "${t}", only a single JAR`);let e=s(t,{withFileTypes:!0}).filter(e=>e.isFile()&&/\.jar$/iu.test(e.name));return e.length===0&&fail(`the directory "${t}" contains no *.jar files`),e.map(e=>e.name).toSorted().map(e=>inspectJar(h(t,e),e))}return n.isFile()||fail(`"${t}" is not a file`),[inspectJar(t,r===void 0?f(t):text(r,`name`))]}function list(e,t){let n=e[t];if(n===void 0)return[];typeof n!=`function`&&fail(`${t} must be a method, got ${show(n)}`);let r=n.call(e);return Array.isArray(r)?r:fail(`${t}() must return an array, got ${show(r)}`)}function contribution(e){if(typeof e==`string`)return{classNames:[className(e)],jars:[],options:[]};let t=e;if(typeof e==`function`)try{t=new e}catch(t){return fail(`could not instantiate the extension class ${e.name||`(anonymous)`}: ${t.message}`)}if(!isRecord(t)||typeof t.getName!=`function`)return fail(`unsupported extension ${show(e)}; expected a fully qualified Java class name, a WireMockExtension or its class`);let n=t,r=text(n.getName(),`getName()`);return within(`extension "${r}"`,()=>{let e=list(n,`getOptions`).map(e=>typeof e==`string`&&B.test(e)?e:fail(`option ${show(e)} must be "--flag" or "--option=value", one option per entry`));e.some(e=>optionName(e)===`--extensions`)&&fail(`set class names with getClassNames(), not "--extensions"`);let t=list(n,`getClassNames`).map(className),i=list(n,`getJars`).flatMap(resolveJars);return t.length+i.length+e.length===0&&fail(`it contributes nothing: implement getClassNames(), getJars() or getOptions()`),{name:r,classNames:t,jars:i,options:e}})}const optionName=e=>e.split(`=`,1)[0];function mergeOptions(e,t,n){let r=[...e];for(let e of t){if(r.includes(e))continue;let t=r.find(t=>optionName(t)===optionName(e));t&&fail(`option "${e}" conflicts with "${t}" set by ${n}; WireMock accepts each option only once`),r.push(e)}return r}const simpleName=e=>e.slice(Math.max(e.lastIndexOf(`.`),e.lastIndexOf(`$`))+1).toLowerCase();function verifyClassNames(e,t){for(let n of e){if(V.some(e=>n.startsWith(e))||t.some(e=>e.classes.includes(n)))continue;let e=t.flatMap(e=>e.classes.filter(e=>simpleName(e)===simpleName(n)));fail(`class "${n}" is in ${t.length?`none of the registered extension JARs (${t.map(e=>f(e.target)).join(`, `)})`:`no JAR: none is registered`}${e.length?`; did you mean "${e.join(`" or "`)}"?`:`.`} Register its JAR with withExtensionJar(), or – if the image already provides it – skip this check with withExtensionOptions({ verifyClassNames: false })`)}}const H=`WIREMOCK_NO_DOCKER`,U=[`CI`,`CONTINUOUS_INTEGRATION`,`GITHUB_ACTIONS`,`GITLAB_CI`,`CIRCLECI`,`BUILDKITE`,`JENKINS_URL`,`TF_BUILD`,`TEAMCITY_VERSION`,`BITBUCKET_BUILD_NUMBER`,`CODEBUILD_BUILD_ID`],W=`127.0.0.1`,G=[`-XX:+IgnoreUnrecognizedVMOptions`,`-XX:TieredStopAtLevel=1`];function detectCI(e=process.env){return U.find(t=>{let n=e[t]?.trim().toLowerCase();return n!==void 0&&n!==``&&n!==`false`&&n!==`0`})}function isNoDockerRequested(e=process.env){let t=e[H]?.trim().toLowerCase();return t===`1`||t===`true`}function installNoDockerRuntime(e={}){let t=e.env??process.env,n=detectCI(t);if(n)throw Error(`--no-docker must not be used in CI/CD (detected ${n}=${t[n]}): it does not run WireMock in a container. Run the E2E suite with Docker instead.`);let r=new NoDockerRuntime({jar:resolveJar(e.jar??t.WIREMOCK_JAR),java:e.java??t.WIREMOCK_JAVA??(t.JAVA_HOME?h(t.JAVA_HOME,`bin`,`java`):`java`),startupTimeout:e.startupTimeout??6e4,cacheDir:e.cacheDir??h(A(),`wiremock-testcontainers`),log:e.log??(e=>process.emitWarning(e,{code:`WIREMOCK_NO_DOCKER`}))});return setRuntime(r),()=>{getRuntime()===r&&setRuntime(void 0)}}function resolveJar(t){if(t){let e=v(t);if(!r(e))throw Error(`--no-docker: WireMock JAR "${e}" does not exist`);return e}let n;try{n=h(m(e(h(process.cwd(),`noop.js`)).resolve(`wiremock/package.json`)),`build`)}catch{throw Error('--no-docker: no WireMock standalone JAR found – install the "wiremock" npm package (e.g. `npm i -D wiremock`) or set WIREMOCK_JAR to a wiremock-standalone-*.jar')}let i=r(n)?s(n).find(e=>/^wiremock-standalone-.+\.jar$/u.test(e)):void 0;if(!i)throw Error(`--no-docker: the "wiremock" npm package contains no wiremock-standalone-*.jar in "${n}"`);return h(n,i)}function classData(e,t,n){if(e!==!1)try{i(e,{recursive:!0});let{size:a,mtimeMs:o}=u(t),c=E(`sha256`).update(`${t}\0${a}\0${o}\0${n}`).digest(`hex`).slice(0,16),d=h(e,`wiremock-${c}.jsa`);if(r(d))return{path:d};for(let t of s(e)){let n=h(e,t);t.endsWith(`.tmp`)&&Date.now()-u(n).mtimeMs>36e5&&l(n,{force:!0})}return{path:d,recording:`${d}.${D()}.tmp`}}catch{return}}var NoDockerRuntime=class{#e;#t;constructor(e){this.#e=e}get mainClass(){return this.#t??=mainClass(o(this.#e.jar))??`wiremock.Run`,this.#t}async start(e){let t=a(h(A(),`wiremock-no-docker-`));try{await materialize(e,t);let n=h(t,`/home/wiremock`);for(let e of[`mappings`,`__files`])i(h(n,e),{recursive:!0});let r=new NoDockerContainer(e,this.#e,t,n,await freePort(),()=>this.mainClass);return await r.launch(),this.#e.log(r.describe()),r}catch(e){throw l(t,{recursive:!0,force:!0}),e}}};async function materialize(e,r){let at=e=>{let t=h(r,e);return i(m(t),{recursive:!0}),t};for(let{source:t,target:r}of e.directories)n(t,at(r),{recursive:!0});for(let{source:n,target:r}of e.files)t(n,at(r));let a=await Promise.all(e.contents.map(async({content:e,target:t})=>({target:t,bytes:e instanceof j?await M(e):e})));for(let{target:e,bytes:t}of a)d(at(e),t)}function freePort(){return new Promise((e,t)=>{let n=k();n.once(`error`,t),n.listen(0,W,()=>{let{port:t}=n.address();n.close(()=>e(t))})})}async function isHealthy(e){try{let t=await fetch(`${e}/__admin/health`);return t.status===404?(await fetch(`${e}/__admin/mappings`)).ok:t.ok}catch{return!1}}const split=e=>e?.split(/\s+/u).filter(Boolean)??[],K=/* @__PURE__ */ new Set;let q=!1;const killOnExit=e=>{K.add(e),q||(q=!0,process.once(`exit`,()=>{for(let e of K)e.kill(`SIGKILL`)}))},unsupported=e=>/* @__PURE__ */ Error(`${e}() is not supported with --no-docker: WireMock runs as a local JVM process, not in a container`);var NoDockerContainer=class{#e;#t;#n;#r;#i;#a=`no-docker-${D()}`;#o;#s;#c;#l;#u=``;constructor(e,t,n,r,i,a){this.#e=e,this.#t=t,this.#n=n,this.#r=r,this.#i=i,this.#o=a}#d(){let e=h(this.#n,P);return r(e)&&s(e).some(e=>/\.jar$/iu.test(e))?[`-cp`,[this.#t.jar,h(e,`*`)].join(p),this.#o()]:[`-jar`,this.#t.jar]}async launch(){let{environment:e,command:t}=this.#e,n=this.#d(),r=classData(this.#t.cacheDir,this.#t.jar,this.#t.java),i=this.#c=r?.recording&&n[0]===`-cp`?void 0:r,a=[...G,...i?[i.recording?`-XX:ArchiveClassesAtExit=${i.recording}`:`-XX:SharedArchiveFile=${i.path}`]:[],...split(e.JAVA_OPTS),...n,`--port`,String(this.#i),`--bind-address`,W,`--root-dir`,this.#r,...t,...split(e.WIREMOCK_OPTIONS)];this.#u=``,this.#l=void 0;let o=T(this.#t.java,a,{env:{...process.env,...e},stdio:[`ignore`,`pipe`,`pipe`]});this.#s=o,o.once(`error`,e=>{this.#l=e});let record=e=>{this.#u=(this.#u+e.toString()).slice(-65536)};o.stdout?.on(`data`,record),o.stderr?.on(`data`,record),killOnExit(o);try{await this.#f(o)}catch(e){throw await this.#p(),e}i&&!i.recording&&/\[warning\]\[cds/u.test(this.#u)&&l(i.path,{force:!0})}async#f(e){let t=Date.now()+this.#t.startupTimeout,output=()=>this.#u.trim()?`\n--- WireMock output ---\n${this.#u.trim()}`:``,poll=async()=>{if(this.#l){let e=this.#l.code===`ENOENT`;throw Error(e?`--no-docker: Java executable "${this.#t.java}" not found – WireMock's standalone JAR needs a Java 11+ runtime; install one or set WIREMOCK_JAVA / JAVA_HOME`:`--no-docker: could not start "${this.#t.java}": ${this.#l.message}`,{cause:this.#l})}if(e.exitCode!==null||e.signalCode!==null)throw Error(`--no-docker: WireMock exited during start-up (${e.signalCode??`exit code ${e.exitCode}`})${output()}`);if(!await isHealthy(this.#m)){if(Date.now()>=t)throw Error(`--no-docker: WireMock was not healthy within ${this.#t.startupTimeout}ms${output()}`);return await new Promise(e=>setTimeout(e,25)),poll()}};await poll()}async#p(){let e=this.#s,t=this.#c?.recording;if(K.delete(e),e?.pid&&e.exitCode===null&&e.signalCode===null){let n=O(e,`exit`);e.kill(t?`SIGTERM`:`SIGKILL`);let r=setTimeout(()=>e.kill(`SIGKILL`),1e4);await n,clearTimeout(r)}if(t){try{e?.exitCode===143&&r(t)&&c(t,this.#c.path)}catch{}l(t,{force:!0}),this.#c=void 0}}get#m(){return`http://${W}:${this.#i}`}describe(){let e=/wiremock-standalone-(.+)\.jar$/u.exec(this.#t.jar)?.[1]??`unknown`,{image:t}=this.#e,n=t.slice(t.lastIndexOf(`:`)+1),r=/^(\d+\.\d+\.\d+)/u.exec(n)?.[1],i=r&&r!==e?` The image tag "${n}" differs from the JAR version ${e}.`:``;return`--no-docker: "${this.#e.image}" is NOT running in a container – WireMock ${e} standalone (local JVM, pid ${this.#s?.pid}) serves ${this.#m} instead.${i} Local development only; never use --no-docker in CI/CD.`}async stop(){await this.#p(),l(this.#n,{recursive:!0,force:!0});let e=this.#a;return{getId:()=>e,copyArchiveFromContainer:async()=>Promise.reject(unsupported(`copyArchiveFromContainer`))}}async restart(){await this.#p(),await this.launch()}async[Symbol.asyncDispose](){await this.stop()}getHost(){return W}getHostname(){return this.getName()}getFirstMappedPort(){return this.#i}getMappedPort(e,t=`tcp`){let n=typeof e==`number`?`${e}/${t}`:e;if(n!==`8080/tcp`)throw Error(`No port mapping found for "${n}"`);return this.#i}getName(){return this.#e.name??`wiremock-no-docker-${this.#i}`}getLabels(){return{}}getId(){return this.#a}getNetworkNames(){return[]}async logs(){return j.from([this.#u])}getNetworkId(){throw unsupported(`getNetworkId`)}getIpAddress(){throw unsupported(`getIpAddress`)}async commit(){throw unsupported(`commit`)}async exec(){throw unsupported(`exec`)}async copyArchiveFromContainer(){throw unsupported(`copyArchiveFromContainer`)}async copyArchiveToContainer(){throw unsupported(`copyArchiveToContainer`)}async copyDirectoriesToContainer(){throw unsupported(`copyDirectoriesToContainer`)}async copyFilesToContainer(){throw unsupported(`copyFilesToContainer`)}async copyContentToContainer(){throw unsupported(`copyContentToContainer`)}};const J=`/home/wiremock/mappings`,Y=`/home/wiremock/__files`,X=`inline JSON`;var WiremockContainer=class extends x{#e=/* @__PURE__ */ new Map;#t=/* @__PURE__ */ new Map;#n=/* @__PURE__ */ new Set;#r=/* @__PURE__ */ new Map;#i=/* @__PURE__ */ new Map;#a=[];#o={scanning:!0,verifyClassNames:!0};#s;constructor(e=`wiremock/wiremock`){super(e),this.withExposedPorts(8080),this.withWaitStrategy(S.forListeningPorts())}withCommand(e){return this.#s=e,super.withCommand(e)}withMappings(e){return within(`WiremockContainer.withMappings`,()=>{let t=h(e,`mappings`);r(t)||fail(`"${t}" does not exist`),u(t).isDirectory()||fail(`"${t}" is not a directory`);let n=h(e,`__files`),i=[{source:t,target:J},...r(n)?[{source:n,target:Y}]:[]];this.#m(i.flatMap(({source:e,target:t})=>listFiles(e).map(n=>({target:g.join(t,n),origin:v(e,n),ids:t===J?stubIdsOf(h(e,n)):[]})))),this.withCopyDirectoriesToContainer(i)}),this}withMapping(e,t){return within(`WiremockContainer.withMapping("${e}")`,()=>{let n=containerName(e),r=g.join(J,n.endsWith(`.json`)?n:`${n}.json`),i=isRecord(t)&&`json`in t,a=i?X:resolveFile(t),s=i?inlineJson(t.json):o(a),c=within(a,()=>stubIds(s.toString()));this.withCopyContentToContainer(this.#m([{target:r,origin:a,ids:c}]).map(()=>({content:s,target:r})))}),this}withFiles(...e){return within(`WiremockContainer.withFiles`,()=>{let t=e.map(e=>{let t=resolveFile(e),n=isRecord(e)&&e.name!==void 0?containerName(e.name):f(t);return{target:g.join(Y,n),origin:t}});this.withCopyFilesToContainer(this.#m(t).map(({origin:e,target:t})=>({source:e,target:t})))}),this}withExtension(e){return this.#c(`withExtension`,[e])}withExtensions(e){return this.#c(`withExtensions`,e)}withExtensionJar(e){return this.#l(`withExtensionJar`,[e])}withExtensionJars(e){return this.#l(`withExtensionJars`,e)}withExtensionOptions(e){return within(`WiremockContainer.withExtensionOptions`,()=>{let t=Object.entries(isRecord(e)?e:fail(`expected an options object, got ${show(e)}`));for(let[e,n]of t)e in this.#o||fail(`unknown option "${e}"; expected ${Object.keys(this.#o).join(` or `)}`),n!==void 0&&typeof n!=`boolean`&&fail(`"${e}" must be a boolean, got ${show(n)}`);this.#o={...this.#o,...Object.fromEntries(t.filter(([,e])=>e!==void 0))}}),this}async start(){let e=within(`WiremockContainer.start`,()=>this.#d());this.createOpts.Cmd=e.length>0?[...this.#s??[],...e]:this.#s?.slice();let t=getRuntime();try{let e=t?await t.start(this.#p()):await super.start();return new StartedWiremockContainer(e,`http://${e.getHost()}:${e.getMappedPort(8080)}`)}catch(e){throw this.#f(e)}}#c(e,t){return within(`WiremockContainer.${e}`,()=>{let e=(Array.isArray(t)?t:fail(`expected an array, got ${show(t)}`)).map(contribution),n=new Map(this.#r),r=this.#a;for(let{name:t,classNames:i,jars:a,options:o}of e){if(t===void 0)continue;let e=JSON.stringify([i,a.map(({origin:e,target:t})=>[e,t]),o]);(n.get(t)??e)!==e&&fail(`another extension named "${t}" is already registered; names must be unique`),n.set(t,e),r=within(`extension "${t}"`,()=>mergeOptions(r,o,`another extension`))}this.#u(e.flatMap(({jars:e})=>e)),[this.#r,this.#a]=[n,r];for(let t of e.flatMap(({classNames:e})=>e))this.#n.add(t)}),this}#l(e,t){return within(`WiremockContainer.${e}`,()=>this.#u((Array.isArray(t)?t:fail(`expected an array, got ${show(t)}`)).flatMap(resolveJars))),this}#u(e){let t=this.#m(e);for(let e of t)this.#i.set(e.target,e);this.withCopyFilesToContainer(t.map(({origin:e,target:t})=>({source:e,target:t})))}#d(){let{scanning:e,verifyClassNames:t}=this.#o;t&&verifyClassNames(this.#n,[...this.#i.values()]);let n=[...this.#s??[],...this.environment.WIREMOCK_OPTIONS?.split(/\s+/u).filter(Boolean)??[]],r=mergeOptions(n,[...this.#a,...e?[]:[`--disable-extensions-scanning`]],`withCommand() or WIREMOCK_OPTIONS`).slice(n.length);if(this.#n.size===0)return r;let i=n.find(e=>optionName(e)===`--extensions`);return i&&fail(`"${i}" is already set by withCommand() or WIREMOCK_OPTIONS; WireMock accepts it only once – register those classes with withExtensions() instead`),[...r,`--extensions`,[...this.#n].join(`,`)]}#f(e){if(this.#n.size+this.#i.size===0)return e;let t=[...this.#i.keys()].map(e=>f(e));return Error(`${e.message}\nWireMock was started with extensions: ${[...this.#n].join(`, `)||`none`}; JARs: ${t.join(`, `)||`none`}. An extension stops WireMock if its class is missing (ClassNotFoundException), lacks a public no-argument constructor or targets a newer Java than the image's; WireMock's output names the cause (Docker: run with DEBUG=testcontainers:containers).`,{cause:e})}#p(){return{image:this.imageName.string,name:this.createOpts.name,command:[...this.createOpts.Cmd??[]],environment:{...this.environment},directories:this.directoriesToCopy.map(({source:e,target:t})=>({source:e,target:t})),files:this.filesToCopy.map(({source:e,target:t})=>({source:e,target:t})),contents:this.contentsToCopy.map(({content:e,target:t})=>({content:e,target:t}))}}#m(e){let t=new Map(this.#e),n=new Map(this.#t),r=e.filter(({target:e,origin:r,ids:i=[]})=>{let a=t.get(e);if(a===r&&r!==X)return!1;a&&fail(`"${e}" is already registered from ${a}; registering ${r} would overwrite it`);for(let t of i){let i=n.get(t);i&&fail(`stub id "${t}" in ${r} is already used by "${i}"; WireMock refuses to start with duplicate stub ids`),n.set(t,e)}return t.set(e,r),!0});return[this.#e,this.#t]=[t,n],r}};const Z=`/__admin`;var StartedWiremockContainer=class extends b{#e;constructor(e,t){super(e),this.#e=t}get rpcUrl(){return this.#e}getUrl(e=``){if(e===``)return this.#e;let t=e.startsWith(`/`)||e.startsWith(`?`)?e:`/${e}`;return`${this.#e}${t}`}getAdminUrl(){return`${this.#e}${Z}`}getMappingUrl(){return`${this.getAdminUrl()}/mappings`}async isRunning(){try{let e=await fetch(`${this.getAdminUrl()}/health`);return e.status===404?(await fetch(this.getMappingUrl())).ok:e.ok}catch{return!1}}async waitUntilReady(e={}){let{timeout:t=3e4,interval:n=250}=e,r=Date.now()+t,poll=async()=>{if(!await this.isRunning()){if(Date.now()>=r)throw Error(`WireMock at ${this.#e} was not ready within ${t}ms`);return await new Promise(e=>setTimeout(e,n)),poll()}};await poll()}async reset(){await this.#t(`POST`,`/reset`)}async resetRequests(){await this.#t(`DELETE`,`/requests`)}async countRequests(e){let{count:t}=await(await this.#t(`POST`,`/requests/count`,e)).json();return t}async findRequests(e){let{requests:t}=await(await this.#t(`POST`,`/requests/find`,e)).json();return t??[]}async#t(e,t,n){let r=await fetch(`${this.getAdminUrl()}${t}`,{method:e,...n===void 0?{}:{headers:{"Content-Type":`application/json`},body:JSON.stringify(n)}});if(!r.ok){let n=await r.text().catch(()=>``);throw Error(`WireMock admin request ${e} ${Z}${t} failed with ${r.status} ${r.statusText}${n?`: ${n}`:``}`)}return r}};export{H as NO_DOCKER_ENV,StartedWiremockContainer,WiremockContainer,detectCI,installNoDockerRuntime,isNoDockerRequested};
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"mock",
|
|
15
15
|
"api"
|
|
16
16
|
],
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.4.0",
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"./no-docker": {
|
|
43
43
|
"import": {
|
|
44
|
-
"types": "./dist/
|
|
45
|
-
"default": "./dist/
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"default": "./dist/index.js"
|
|
46
46
|
},
|
|
47
47
|
"require": {
|
|
48
|
-
"types": "./dist/
|
|
49
|
-
"default": "./dist/
|
|
48
|
+
"types": "./dist/index.d.cts",
|
|
49
|
+
"default": "./dist/index.cjs"
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"./package.json": "./package.json"
|
|
@@ -83,10 +83,12 @@
|
|
|
83
83
|
"test:unit": "vitest run test/unit",
|
|
84
84
|
"test:e2e": "node scripts/test-e2e.mjs",
|
|
85
85
|
"test:coverage": "vitest run --coverage",
|
|
86
|
+
"build:extensions": "node scripts/build-extensions.mjs",
|
|
86
87
|
"lint": "oxlint --deny-warnings",
|
|
87
88
|
"typecheck": "tsc --noEmit",
|
|
88
89
|
"check": "pnpm build && pnpm typecheck && pnpm lint && pnpm test:coverage",
|
|
89
90
|
"examples:setup": "pnpm build && npm link && cd examples && npm install && npm run link:library",
|
|
91
|
+
"examples:unit": "pnpm examples:setup && cd examples && npm run typecheck && npm run test:unit",
|
|
90
92
|
"examples:e2e": "pnpm examples:setup && cd examples && npm run typecheck && npm test"
|
|
91
93
|
}
|
|
92
94
|
}
|
package/dist/no-docker.cjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const require_runtime=require("./runtime-6blQsklJ.cjs");let node_fs=require("node:fs");let node_path=require("node:path");let node_module=require("node:module");let node_child_process=require("node:child_process");let node_crypto=require("node:crypto");let node_events=require("node:events");let node_net=require("node:net");let node_os=require("node:os");let node_stream=require("node:stream");let node_stream_consumers=require("node:stream/consumers");const NO_DOCKER_ENV=`WIREMOCK_NO_DOCKER`;const CI_VARIABLES=[`CI`,`CONTINUOUS_INTEGRATION`,`GITHUB_ACTIONS`,`GITLAB_CI`,`CIRCLECI`,`BUILDKITE`,`JENKINS_URL`,`TF_BUILD`,`TEAMCITY_VERSION`,`BITBUCKET_BUILD_NUMBER`,`CODEBUILD_BUILD_ID`];const WIREMOCK_HOME=`/home/wiremock`;const WIREMOCK_PORT=8080;const HOST=`127.0.0.1`;const POLL_INTERVAL=25;const FAST_START=[`-XX:+IgnoreUnrecognizedVMOptions`,`-XX:TieredStopAtLevel=1`];const GRACEFUL_EXIT=143;const STALE_RECORDING=36e5;function detectCI(env=process.env){return CI_VARIABLES.find(name=>{const value=env[name]?.trim().toLowerCase();return value!==void 0&&value!==``&&value!==`false`&&value!==`0`})}function isNoDockerRequested(env=process.env){const value=env[NO_DOCKER_ENV]?.trim().toLowerCase();return value===`1`||value===`true`}function installNoDockerRuntime(options={}){const env=options.env??process.env;const ci=detectCI(env);if(ci)throw new Error(`--no-docker must not be used in CI/CD (detected ${ci}=${env[ci]}): it does not run WireMock in a container. Run the E2E suite with Docker instead.`);const runtime=new NoDockerRuntime({jar:resolveJar(options.jar??env.WIREMOCK_JAR),java:options.java??env.WIREMOCK_JAVA??(env.JAVA_HOME?(0,node_path.join)(env.JAVA_HOME,`bin`,`java`):`java`),startupTimeout:options.startupTimeout??6e4,cacheDir:options.cacheDir??(0,node_path.join)((0,node_os.tmpdir)(),`wiremock-testcontainers`),log:options.log??(message=>process.emitWarning(message,{code:`WIREMOCK_NO_DOCKER`}))});require_runtime.n(runtime);return()=>{if(require_runtime.t()===runtime)require_runtime.n(void 0)}}function resolveJar(explicit){if(explicit){const jar=(0,node_path.resolve)(explicit);if(!(0,node_fs.existsSync)(jar))throw new Error(`--no-docker: WireMock JAR "${jar}" does not exist`);return jar}let build;try{build=(0,node_path.join)((0,node_path.dirname)((0,node_module.createRequire)((0,node_path.join)(process.cwd(),`noop.js`)).resolve(`wiremock/package.json`)),`build`)}catch{throw new Error('--no-docker: no WireMock standalone JAR found – install the "wiremock" npm package (e.g. `npm i -D wiremock`) or set WIREMOCK_JAR to a wiremock-standalone-*.jar')}const name=(0,node_fs.existsSync)(build)?(0,node_fs.readdirSync)(build).find(file=>/^wiremock-standalone-.+\.jar$/u.test(file)):void 0;if(!name)throw new Error(`--no-docker: the "wiremock" npm package contains no wiremock-standalone-*.jar in "${build}"`);return(0,node_path.join)(build,name)}function classData(cacheDir,jar,java){if(cacheDir===false)return void 0;try{(0,node_fs.mkdirSync)(cacheDir,{recursive:true});const{size,mtimeMs}=(0,node_fs.statSync)(jar);const key=(0,node_crypto.createHash)(`sha256`).update(`${jar}\0${size}\0${mtimeMs}\0${java}`).digest(`hex`).slice(0,16);const path=(0,node_path.join)(cacheDir,`wiremock-${key}.jsa`);if((0,node_fs.existsSync)(path))return{path};for(const file of(0,node_fs.readdirSync)(cacheDir)){const stale=(0,node_path.join)(cacheDir,file);if(file.endsWith(`.tmp`)&&Date.now()-(0,node_fs.statSync)(stale).mtimeMs>STALE_RECORDING)(0,node_fs.rmSync)(stale,{force:true})}return{path,recording:`${path}.${(0,node_crypto.randomUUID)()}.tmp`}}catch{return}}var NoDockerRuntime=class{#config;constructor(config){this.#config=config}async start(request){const root=(0,node_fs.mkdtempSync)((0,node_path.join)((0,node_os.tmpdir)(),`wiremock-no-docker-`));try{await materialize(request,root);const home=(0,node_path.join)(root,WIREMOCK_HOME);for(const dir of[`mappings`,`__files`])(0,node_fs.mkdirSync)((0,node_path.join)(home,dir),{recursive:true});const container=new NoDockerContainer(request,this.#config,root,home,await freePort());await container.launch();this.#config.log(container.describe());return container}catch(error){(0,node_fs.rmSync)(root,{recursive:true,force:true});throw error}}};async function materialize(request,root){const at=target=>{const path=(0,node_path.join)(root,target);(0,node_fs.mkdirSync)((0,node_path.dirname)(path),{recursive:true});return path};for(const{source,target}of request.directories)(0,node_fs.cpSync)(source,at(target),{recursive:true});for(const{source,target}of request.files)(0,node_fs.copyFileSync)(source,at(target));const contents=await Promise.all(request.contents.map(async({content,target})=>({target,bytes:content instanceof node_stream.Readable?await(0,node_stream_consumers.buffer)(content):content})));for(const{target,bytes}of contents)(0,node_fs.writeFileSync)(at(target),bytes)}function freePort(){return new Promise((_resolve,_reject)=>{const server=(0,node_net.createServer)();server.once(`error`,_reject);server.listen(0,HOST,()=>{const{port}=server.address();server.close(()=>_resolve(port))})})}async function isHealthy(url){try{const health=await fetch(`${url}/__admin/health`);if(health.status!==404)return health.ok;return(await fetch(`${url}/__admin/mappings`)).ok}catch{return false}}const split=value=>value?.split(/\s+/u).filter(Boolean)??[];const live=/* @__PURE__ */ new Set;let exitHookInstalled=false;const killOnExit=child=>{live.add(child);if(exitHookInstalled)return;exitHookInstalled=true;process.once(`exit`,()=>{for(const process of live)process.kill(`SIGKILL`)})};const unsupported=method=>/* @__PURE__ */ new Error(`${method}() is not supported with --no-docker: WireMock runs as a local JVM process, not in a container`);var NoDockerContainer=class{#request;#config;#root;#home;#port;#id=`no-docker-${(0,node_crypto.randomUUID)()}`;#child;#classData;#spawnError;#output=``;constructor(request,config,root,home,port){this.#request=request;this.#config=config;this.#root=root;this.#home=home;this.#port=port}async launch(){const{environment,command}=this.#request;const data=this.#classData=classData(this.#config.cacheDir,this.#config.jar,this.#config.java);const args=[...FAST_START,...data?[data.recording?`-XX:ArchiveClassesAtExit=${data.recording}`:`-XX:SharedArchiveFile=${data.path}`]:[],...split(environment.JAVA_OPTS),`-jar`,this.#config.jar,`--port`,String(this.#port),`--bind-address`,HOST,`--root-dir`,this.#home,...command,...split(environment.WIREMOCK_OPTIONS)];this.#output=``;this.#spawnError=void 0;const child=(0,node_child_process.spawn)(this.#config.java,args,{env:{...process.env,...environment},stdio:[`ignore`,`pipe`,`pipe`]});this.#child=child;child.once(`error`,error=>{this.#spawnError=error});const record=chunk=>{this.#output=(this.#output+chunk.toString()).slice(-65536)};child.stdout?.on(`data`,record);child.stderr?.on(`data`,record);killOnExit(child);try{await this.#waitUntilHealthy(child)}catch(error){await this.#terminate();throw error}if(data&&!data.recording&&/\[warning\]\[cds/u.test(this.#output))(0,node_fs.rmSync)(data.path,{force:true})}async#waitUntilHealthy(child){const deadline=Date.now()+this.#config.startupTimeout;const output=()=>this.#output.trim()?`\n--- WireMock output ---\n${this.#output.trim()}`:``;const poll=async()=>{if(this.#spawnError){const missing=this.#spawnError.code===`ENOENT`;throw new Error(missing?`--no-docker: Java executable "${this.#config.java}" not found – WireMock's standalone JAR needs a Java 11+ runtime; install one or set WIREMOCK_JAVA / JAVA_HOME`:`--no-docker: could not start "${this.#config.java}": ${this.#spawnError.message}`,{cause:this.#spawnError})}if(child.exitCode!==null||child.signalCode!==null)throw new Error(`--no-docker: WireMock exited during start-up (${child.signalCode??`exit code ${child.exitCode}`})${output()}`);if(await isHealthy(this.#url))return;if(Date.now()>=deadline)throw new Error(`--no-docker: WireMock was not healthy within ${this.#config.startupTimeout}ms${output()}`);await new Promise(_resolve=>setTimeout(_resolve,POLL_INTERVAL));return poll()};await poll()}async#terminate(){const child=this.#child;const recording=this.#classData?.recording;live.delete(child);if(child?.pid&&child.exitCode===null&&child.signalCode===null){const exited=(0,node_events.once)(child,`exit`);child.kill(recording?`SIGTERM`:`SIGKILL`);const timer=setTimeout(()=>child.kill(`SIGKILL`),1e4);await exited;clearTimeout(timer)}if(!recording)return;try{if(child?.exitCode===GRACEFUL_EXIT&&(0,node_fs.existsSync)(recording))(0,node_fs.renameSync)(recording,this.#classData.path)}catch{}(0,node_fs.rmSync)(recording,{force:true});this.#classData=void 0}get#url(){return`http://${HOST}:${this.#port}`}describe(){const version=/wiremock-standalone-(.+)\.jar$/u.exec(this.#config.jar)?.[1]??`unknown`;const{image}=this.#request;const tag=image.slice(image.lastIndexOf(`:`)+1);const pinned=/^(\d+\.\d+\.\d+)/u.exec(tag)?.[1];const mismatch=pinned&&pinned!==version?` The image tag "${tag}" differs from the JAR version ${version}.`:``;return`--no-docker: "${this.#request.image}" is NOT running in a container – WireMock ${version} standalone (local JVM, pid ${this.#child?.pid}) serves ${this.#url} instead.${mismatch} Local development only; never use --no-docker in CI/CD.`}async stop(){await this.#terminate();(0,node_fs.rmSync)(this.#root,{recursive:true,force:true});const id=this.#id;return{getId:()=>id,copyArchiveFromContainer:async()=>Promise.reject(unsupported(`copyArchiveFromContainer`))}}async restart(){await this.#terminate();await this.launch()}async[Symbol.asyncDispose](){await this.stop()}getHost(){return HOST}getHostname(){return this.getName()}getFirstMappedPort(){return this.#port}getMappedPort(port,protocol=`tcp`){const key=typeof port===`number`?`${port}/${protocol}`:port;if(key!==`${WIREMOCK_PORT}/tcp`)throw new Error(`No port mapping found for "${key}"`);return this.#port}getName(){return this.#request.name??`wiremock-no-docker-${this.#port}`}getLabels(){return{}}getId(){return this.#id}getNetworkNames(){return[]}async logs(){return node_stream.Readable.from([this.#output])}getNetworkId(){throw unsupported(`getNetworkId`)}getIpAddress(){throw unsupported(`getIpAddress`)}async commit(){throw unsupported(`commit`)}async exec(){throw unsupported(`exec`)}async copyArchiveFromContainer(){throw unsupported(`copyArchiveFromContainer`)}async copyArchiveToContainer(){throw unsupported(`copyArchiveToContainer`)}async copyDirectoriesToContainer(){throw unsupported(`copyDirectoriesToContainer`)}async copyFilesToContainer(){throw unsupported(`copyFilesToContainer`)}async copyContentToContainer(){throw unsupported(`copyContentToContainer`)}};exports.NO_DOCKER_ENV=NO_DOCKER_ENV;exports.detectCI=detectCI;exports.installNoDockerRuntime=installNoDockerRuntime;exports.isNoDockerRequested=isNoDockerRequested;
|
package/dist/no-docker.d.cts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `--no-docker` harness – **local development only**: serves every `WiremockContainer` from a
|
|
3
|
-
* local WireMock standalone JVM instead of Docker. Refuses to run in CI/CD and warns on every
|
|
4
|
-
* start. Needs Java 11+ and a WireMock JAR (e.g. the `wiremock` npm package).
|
|
5
|
-
* @example
|
|
6
|
-
* ```typescript
|
|
7
|
-
* // Vitest/Jest setup file, registered only when `WIREMOCK_NO_DOCKER=1`
|
|
8
|
-
* import { installNoDockerRuntime } from "@hellaweb3/wiremock-testcontainers-nodejs/no-docker";
|
|
9
|
-
* installNoDockerRuntime();
|
|
10
|
-
* ```
|
|
11
|
-
* @module
|
|
12
|
-
*/
|
|
13
|
-
/** Set to `1` by the `--no-docker` flag. */
|
|
14
|
-
export declare const NO_DOCKER_ENV = "WIREMOCK_NO_DOCKER";
|
|
15
|
-
type Environment = Readonly<Record<string, string | undefined>>;
|
|
16
|
-
/** Options for {@link installNoDockerRuntime}. */
|
|
17
|
-
export interface NoDockerOptions {
|
|
18
|
-
/** WireMock standalone JAR. Default: `$WIREMOCK_JAR`, else the `wiremock` npm package's. */
|
|
19
|
-
readonly jar?: string;
|
|
20
|
-
/** Java executable. Default: `$WIREMOCK_JAVA`, `$JAVA_HOME/bin/java`, else `java`. */
|
|
21
|
-
readonly java?: string;
|
|
22
|
-
/** Milliseconds to wait for WireMock to be healthy. Default `60_000`. */
|
|
23
|
-
readonly startupTimeout?: number;
|
|
24
|
-
/** Receives the per-start warning. Default: `process.emitWarning`. */
|
|
25
|
-
readonly log?: (message: string) => void;
|
|
26
|
-
/**
|
|
27
|
-
* Cache for the class-data archive that halves JVM start-up after the first run; `false`
|
|
28
|
-
* disables it. Default: `<os tmpdir>/wiremock-testcontainers`.
|
|
29
|
-
*/
|
|
30
|
-
readonly cacheDir?: string | false;
|
|
31
|
-
/** Source of the CI/CD check and the defaults above. Default: `process.env`. */
|
|
32
|
-
readonly env?: Environment;
|
|
33
|
-
}
|
|
34
|
-
/** The CI/CD variable set in `env`, if any. */
|
|
35
|
-
export declare function detectCI(env?: Environment): string | undefined;
|
|
36
|
-
/** Whether {@link NO_DOCKER_ENV} is `1` or `true`. */
|
|
37
|
-
export declare function isNoDockerRequested(env?: Environment): boolean;
|
|
38
|
-
/**
|
|
39
|
-
* Serves every `WiremockContainer` started from now on from a local WireMock JVM: its mappings,
|
|
40
|
-
* files and command are materialised into a temp directory, WireMock listens on a free port of
|
|
41
|
-
* `127.0.0.1`. Container-only features (`exec`, networks, …) throw.
|
|
42
|
-
* @returns A function that uninstalls it.
|
|
43
|
-
* @throws In CI/CD, or if no WireMock JAR is found.
|
|
44
|
-
*/
|
|
45
|
-
export declare function installNoDockerRuntime(options?: NoDockerOptions): () => void;
|
package/dist/no-docker.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `--no-docker` harness – **local development only**: serves every `WiremockContainer` from a
|
|
3
|
-
* local WireMock standalone JVM instead of Docker. Refuses to run in CI/CD and warns on every
|
|
4
|
-
* start. Needs Java 11+ and a WireMock JAR (e.g. the `wiremock` npm package).
|
|
5
|
-
* @example
|
|
6
|
-
* ```typescript
|
|
7
|
-
* // Vitest/Jest setup file, registered only when `WIREMOCK_NO_DOCKER=1`
|
|
8
|
-
* import { installNoDockerRuntime } from "@hellaweb3/wiremock-testcontainers-nodejs/no-docker";
|
|
9
|
-
* installNoDockerRuntime();
|
|
10
|
-
* ```
|
|
11
|
-
* @module
|
|
12
|
-
*/
|
|
13
|
-
/** Set to `1` by the `--no-docker` flag. */
|
|
14
|
-
export declare const NO_DOCKER_ENV = "WIREMOCK_NO_DOCKER";
|
|
15
|
-
type Environment = Readonly<Record<string, string | undefined>>;
|
|
16
|
-
/** Options for {@link installNoDockerRuntime}. */
|
|
17
|
-
export interface NoDockerOptions {
|
|
18
|
-
/** WireMock standalone JAR. Default: `$WIREMOCK_JAR`, else the `wiremock` npm package's. */
|
|
19
|
-
readonly jar?: string;
|
|
20
|
-
/** Java executable. Default: `$WIREMOCK_JAVA`, `$JAVA_HOME/bin/java`, else `java`. */
|
|
21
|
-
readonly java?: string;
|
|
22
|
-
/** Milliseconds to wait for WireMock to be healthy. Default `60_000`. */
|
|
23
|
-
readonly startupTimeout?: number;
|
|
24
|
-
/** Receives the per-start warning. Default: `process.emitWarning`. */
|
|
25
|
-
readonly log?: (message: string) => void;
|
|
26
|
-
/**
|
|
27
|
-
* Cache for the class-data archive that halves JVM start-up after the first run; `false`
|
|
28
|
-
* disables it. Default: `<os tmpdir>/wiremock-testcontainers`.
|
|
29
|
-
*/
|
|
30
|
-
readonly cacheDir?: string | false;
|
|
31
|
-
/** Source of the CI/CD check and the defaults above. Default: `process.env`. */
|
|
32
|
-
readonly env?: Environment;
|
|
33
|
-
}
|
|
34
|
-
/** The CI/CD variable set in `env`, if any. */
|
|
35
|
-
export declare function detectCI(env?: Environment): string | undefined;
|
|
36
|
-
/** Whether {@link NO_DOCKER_ENV} is `1` or `true`. */
|
|
37
|
-
export declare function isNoDockerRequested(env?: Environment): boolean;
|
|
38
|
-
/**
|
|
39
|
-
* Serves every `WiremockContainer` started from now on from a local WireMock JVM: its mappings,
|
|
40
|
-
* files and command are materialised into a temp directory, WireMock listens on a free port of
|
|
41
|
-
* `127.0.0.1`. Container-only features (`exec`, networks, …) throw.
|
|
42
|
-
* @returns A function that uninstalls it.
|
|
43
|
-
* @throws In CI/CD, or if no WireMock JAR is found.
|
|
44
|
-
*/
|
|
45
|
-
export declare function installNoDockerRuntime(options?: NoDockerOptions): () => void;
|
package/dist/no-docker.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{n as setRuntime,t as getRuntime}from"./runtime-ChyDTLYb.js";import{createRequire}from"node:module";import{copyFileSync,cpSync,existsSync,mkdirSync,mkdtempSync,readdirSync,renameSync,rmSync,statSync,writeFileSync}from"node:fs";import{dirname,join,resolve}from"node:path";import{spawn}from"node:child_process";import{createHash,randomUUID}from"node:crypto";import{once}from"node:events";import{createServer}from"node:net";import{tmpdir}from"node:os";import{Readable}from"node:stream";import{buffer}from"node:stream/consumers";const NO_DOCKER_ENV=`WIREMOCK_NO_DOCKER`;const CI_VARIABLES=[`CI`,`CONTINUOUS_INTEGRATION`,`GITHUB_ACTIONS`,`GITLAB_CI`,`CIRCLECI`,`BUILDKITE`,`JENKINS_URL`,`TF_BUILD`,`TEAMCITY_VERSION`,`BITBUCKET_BUILD_NUMBER`,`CODEBUILD_BUILD_ID`];const WIREMOCK_HOME=`/home/wiremock`;const WIREMOCK_PORT=8080;const HOST=`127.0.0.1`;const POLL_INTERVAL=25;const FAST_START=[`-XX:+IgnoreUnrecognizedVMOptions`,`-XX:TieredStopAtLevel=1`];const GRACEFUL_EXIT=143;const STALE_RECORDING=36e5;function detectCI(env=process.env){return CI_VARIABLES.find(name=>{const value=env[name]?.trim().toLowerCase();return value!==void 0&&value!==``&&value!==`false`&&value!==`0`})}function isNoDockerRequested(env=process.env){const value=env[NO_DOCKER_ENV]?.trim().toLowerCase();return value===`1`||value===`true`}function installNoDockerRuntime(options={}){const env=options.env??process.env;const ci=detectCI(env);if(ci)throw new Error(`--no-docker must not be used in CI/CD (detected ${ci}=${env[ci]}): it does not run WireMock in a container. Run the E2E suite with Docker instead.`);const runtime=new NoDockerRuntime({jar:resolveJar(options.jar??env.WIREMOCK_JAR),java:options.java??env.WIREMOCK_JAVA??(env.JAVA_HOME?join(env.JAVA_HOME,`bin`,`java`):`java`),startupTimeout:options.startupTimeout??6e4,cacheDir:options.cacheDir??join(tmpdir(),`wiremock-testcontainers`),log:options.log??(message=>process.emitWarning(message,{code:`WIREMOCK_NO_DOCKER`}))});setRuntime(runtime);return()=>{if(getRuntime()===runtime)setRuntime(void 0)}}function resolveJar(explicit){if(explicit){const jar=resolve(explicit);if(!existsSync(jar))throw new Error(`--no-docker: WireMock JAR "${jar}" does not exist`);return jar}let build;try{build=join(dirname(createRequire(join(process.cwd(),`noop.js`)).resolve(`wiremock/package.json`)),`build`)}catch{throw new Error('--no-docker: no WireMock standalone JAR found – install the "wiremock" npm package (e.g. `npm i -D wiremock`) or set WIREMOCK_JAR to a wiremock-standalone-*.jar')}const name=existsSync(build)?readdirSync(build).find(file=>/^wiremock-standalone-.+\.jar$/u.test(file)):void 0;if(!name)throw new Error(`--no-docker: the "wiremock" npm package contains no wiremock-standalone-*.jar in "${build}"`);return join(build,name)}function classData(cacheDir,jar,java){if(cacheDir===false)return void 0;try{mkdirSync(cacheDir,{recursive:true});const{size,mtimeMs}=statSync(jar);const key=createHash(`sha256`).update(`${jar}\0${size}\0${mtimeMs}\0${java}`).digest(`hex`).slice(0,16);const path=join(cacheDir,`wiremock-${key}.jsa`);if(existsSync(path))return{path};for(const file of readdirSync(cacheDir)){const stale=join(cacheDir,file);if(file.endsWith(`.tmp`)&&Date.now()-statSync(stale).mtimeMs>STALE_RECORDING)rmSync(stale,{force:true})}return{path,recording:`${path}.${randomUUID()}.tmp`}}catch{return}}var NoDockerRuntime=class{#config;constructor(config){this.#config=config}async start(request){const root=mkdtempSync(join(tmpdir(),`wiremock-no-docker-`));try{await materialize(request,root);const home=join(root,WIREMOCK_HOME);for(const dir of[`mappings`,`__files`])mkdirSync(join(home,dir),{recursive:true});const container=new NoDockerContainer(request,this.#config,root,home,await freePort());await container.launch();this.#config.log(container.describe());return container}catch(error){rmSync(root,{recursive:true,force:true});throw error}}};async function materialize(request,root){const at=target=>{const path=join(root,target);mkdirSync(dirname(path),{recursive:true});return path};for(const{source,target}of request.directories)cpSync(source,at(target),{recursive:true});for(const{source,target}of request.files)copyFileSync(source,at(target));const contents=await Promise.all(request.contents.map(async({content,target})=>({target,bytes:content instanceof Readable?await buffer(content):content})));for(const{target,bytes}of contents)writeFileSync(at(target),bytes)}function freePort(){return new Promise((_resolve,_reject)=>{const server=createServer();server.once(`error`,_reject);server.listen(0,HOST,()=>{const{port}=server.address();server.close(()=>_resolve(port))})})}async function isHealthy(url){try{const health=await fetch(`${url}/__admin/health`);if(health.status!==404)return health.ok;return(await fetch(`${url}/__admin/mappings`)).ok}catch{return false}}const split=value=>value?.split(/\s+/u).filter(Boolean)??[];const live=/* @__PURE__ */ new Set;let exitHookInstalled=false;const killOnExit=child=>{live.add(child);if(exitHookInstalled)return;exitHookInstalled=true;process.once(`exit`,()=>{for(const process of live)process.kill(`SIGKILL`)})};const unsupported=method=>/* @__PURE__ */ new Error(`${method}() is not supported with --no-docker: WireMock runs as a local JVM process, not in a container`);var NoDockerContainer=class{#request;#config;#root;#home;#port;#id=`no-docker-${randomUUID()}`;#child;#classData;#spawnError;#output=``;constructor(request,config,root,home,port){this.#request=request;this.#config=config;this.#root=root;this.#home=home;this.#port=port}async launch(){const{environment,command}=this.#request;const data=this.#classData=classData(this.#config.cacheDir,this.#config.jar,this.#config.java);const args=[...FAST_START,...data?[data.recording?`-XX:ArchiveClassesAtExit=${data.recording}`:`-XX:SharedArchiveFile=${data.path}`]:[],...split(environment.JAVA_OPTS),`-jar`,this.#config.jar,`--port`,String(this.#port),`--bind-address`,HOST,`--root-dir`,this.#home,...command,...split(environment.WIREMOCK_OPTIONS)];this.#output=``;this.#spawnError=void 0;const child=spawn(this.#config.java,args,{env:{...process.env,...environment},stdio:[`ignore`,`pipe`,`pipe`]});this.#child=child;child.once(`error`,error=>{this.#spawnError=error});const record=chunk=>{this.#output=(this.#output+chunk.toString()).slice(-65536)};child.stdout?.on(`data`,record);child.stderr?.on(`data`,record);killOnExit(child);try{await this.#waitUntilHealthy(child)}catch(error){await this.#terminate();throw error}if(data&&!data.recording&&/\[warning\]\[cds/u.test(this.#output))rmSync(data.path,{force:true})}async#waitUntilHealthy(child){const deadline=Date.now()+this.#config.startupTimeout;const output=()=>this.#output.trim()?`\n--- WireMock output ---\n${this.#output.trim()}`:``;const poll=async()=>{if(this.#spawnError){const missing=this.#spawnError.code===`ENOENT`;throw new Error(missing?`--no-docker: Java executable "${this.#config.java}" not found – WireMock's standalone JAR needs a Java 11+ runtime; install one or set WIREMOCK_JAVA / JAVA_HOME`:`--no-docker: could not start "${this.#config.java}": ${this.#spawnError.message}`,{cause:this.#spawnError})}if(child.exitCode!==null||child.signalCode!==null)throw new Error(`--no-docker: WireMock exited during start-up (${child.signalCode??`exit code ${child.exitCode}`})${output()}`);if(await isHealthy(this.#url))return;if(Date.now()>=deadline)throw new Error(`--no-docker: WireMock was not healthy within ${this.#config.startupTimeout}ms${output()}`);await new Promise(_resolve=>setTimeout(_resolve,POLL_INTERVAL));return poll()};await poll()}async#terminate(){const child=this.#child;const recording=this.#classData?.recording;live.delete(child);if(child?.pid&&child.exitCode===null&&child.signalCode===null){const exited=once(child,`exit`);child.kill(recording?`SIGTERM`:`SIGKILL`);const timer=setTimeout(()=>child.kill(`SIGKILL`),1e4);await exited;clearTimeout(timer)}if(!recording)return;try{if(child?.exitCode===GRACEFUL_EXIT&&existsSync(recording))renameSync(recording,this.#classData.path)}catch{}rmSync(recording,{force:true});this.#classData=void 0}get#url(){return`http://${HOST}:${this.#port}`}describe(){const version=/wiremock-standalone-(.+)\.jar$/u.exec(this.#config.jar)?.[1]??`unknown`;const{image}=this.#request;const tag=image.slice(image.lastIndexOf(`:`)+1);const pinned=/^(\d+\.\d+\.\d+)/u.exec(tag)?.[1];const mismatch=pinned&&pinned!==version?` The image tag "${tag}" differs from the JAR version ${version}.`:``;return`--no-docker: "${this.#request.image}" is NOT running in a container – WireMock ${version} standalone (local JVM, pid ${this.#child?.pid}) serves ${this.#url} instead.${mismatch} Local development only; never use --no-docker in CI/CD.`}async stop(){await this.#terminate();rmSync(this.#root,{recursive:true,force:true});const id=this.#id;return{getId:()=>id,copyArchiveFromContainer:async()=>Promise.reject(unsupported(`copyArchiveFromContainer`))}}async restart(){await this.#terminate();await this.launch()}async[Symbol.asyncDispose](){await this.stop()}getHost(){return HOST}getHostname(){return this.getName()}getFirstMappedPort(){return this.#port}getMappedPort(port,protocol=`tcp`){const key=typeof port===`number`?`${port}/${protocol}`:port;if(key!==`${WIREMOCK_PORT}/tcp`)throw new Error(`No port mapping found for "${key}"`);return this.#port}getName(){return this.#request.name??`wiremock-no-docker-${this.#port}`}getLabels(){return{}}getId(){return this.#id}getNetworkNames(){return[]}async logs(){return Readable.from([this.#output])}getNetworkId(){throw unsupported(`getNetworkId`)}getIpAddress(){throw unsupported(`getIpAddress`)}async commit(){throw unsupported(`commit`)}async exec(){throw unsupported(`exec`)}async copyArchiveFromContainer(){throw unsupported(`copyArchiveFromContainer`)}async copyArchiveToContainer(){throw unsupported(`copyArchiveToContainer`)}async copyDirectoriesToContainer(){throw unsupported(`copyDirectoriesToContainer`)}async copyFilesToContainer(){throw unsupported(`copyFilesToContainer`)}async copyContentToContainer(){throw unsupported(`copyContentToContainer`)}};export{NO_DOCKER_ENV,detectCI,installNoDockerRuntime,isNoDockerRequested};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const RUNTIME_KEY=Symbol.for(`@hellaweb3/wiremock-testcontainers-nodejs/runtime`);const registry=globalThis;const getRuntime=()=>registry[RUNTIME_KEY];const setRuntime=runtime=>{if(runtime)registry[RUNTIME_KEY]=runtime;else delete registry[RUNTIME_KEY]};Object.defineProperty(exports,"n",{enumerable:true,get:function(){return setRuntime}});Object.defineProperty(exports,"t",{enumerable:true,get:function(){return getRuntime}});
|
package/dist/runtime-ChyDTLYb.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const RUNTIME_KEY=Symbol.for(`@hellaweb3/wiremock-testcontainers-nodejs/runtime`);const registry=globalThis;const getRuntime=()=>registry[RUNTIME_KEY];const setRuntime=runtime=>{if(runtime)registry[RUNTIME_KEY]=runtime;else delete registry[RUNTIME_KEY]};export{setRuntime as n,getRuntime as t};
|