nuxt-api-contract 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -7
- package/dist/chunks/server.mjs +162 -0
- package/dist/cli.mjs +33 -1
- package/dist/client.d.mts +5 -69
- package/dist/client.d.ts +5 -69
- package/dist/client.mjs +3 -1
- package/dist/composables.mjs +2 -1
- package/dist/mock.d.mts +54 -0
- package/dist/mock.d.ts +54 -0
- package/dist/mock.mjs +8 -0
- package/dist/module.d.mts +8 -2
- package/dist/module.d.ts +8 -2
- package/dist/module.mjs +4 -2
- package/dist/runtime/shared/mock.mjs +188 -0
- package/dist/server.d.mts +7 -4
- package/dist/server.d.ts +7 -4
- package/dist/server.mjs +6 -3
- package/dist/shared/nuxt-api-contract.BOxyDvRy.d.mts +40 -0
- package/dist/shared/{nuxt-api-contract.S1zqCiJX.d.ts → nuxt-api-contract.B_UvrNC8.d.ts} +1 -1
- package/dist/shared/nuxt-api-contract.Bd2Y7Lx0.mjs +191 -0
- package/dist/shared/nuxt-api-contract.BgPd-YXc.d.mts +53 -0
- package/dist/shared/nuxt-api-contract.BuWjAHJe.d.ts +40 -0
- package/dist/shared/nuxt-api-contract.C7KxMQHa.d.ts +53 -0
- package/dist/shared/{nuxt-api-contract.CPm9WbWA.d.mts → nuxt-api-contract.CFG8gzJH.d.mts} +2 -2
- package/dist/shared/{nuxt-api-contract.CPm9WbWA.d.ts → nuxt-api-contract.CFG8gzJH.d.ts} +2 -2
- package/dist/shared/nuxt-api-contract.DZfOzVaB.d.mts +16 -0
- package/dist/shared/nuxt-api-contract.DZfOzVaB.d.ts +16 -0
- package/dist/shared/nuxt-api-contract.Dr4tPqGB.mjs +38 -0
- package/dist/shared/{nuxt-api-contract.QDGSGaVY.mjs → nuxt-api-contract.V15soI_f.mjs} +1 -38
- package/dist/shared/{nuxt-api-contract.B9JBCRk8.d.mts → nuxt-api-contract.VCevNWQV.d.mts} +1 -1
- package/dist/shared.d.mts +4 -2
- package/dist/shared.d.ts +4 -2
- package/dist/shared.mjs +3 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -237,13 +237,38 @@ generation never fails.
|
|
|
237
237
|
## Mocking
|
|
238
238
|
|
|
239
239
|
```ts
|
|
240
|
-
import { mockContract } from 'nuxt-api-contract/client'
|
|
240
|
+
import { mockContract, autoMockContract } from 'nuxt-api-contract/client'
|
|
241
241
|
|
|
242
242
|
mockContract(GetUser, { response: () => ({ id: '1', name: 'Mocked User' }) })
|
|
243
|
+
autoMockContract(GetUser, { seed: 42 }) // generated from the response schema
|
|
243
244
|
```
|
|
244
245
|
|
|
245
|
-
Enable `apiContract: { mocks: true }` and contract handlers return
|
|
246
|
-
|
|
246
|
+
Enable `apiContract: { mocks: true | 'auto' }` and contract handlers return
|
|
247
|
+
mock responses (still validated against the response schema). With `'auto'`,
|
|
248
|
+
contracts without an explicit `mockContract()` fall back to generated data.
|
|
249
|
+
|
|
250
|
+
### Standalone mock server
|
|
251
|
+
|
|
252
|
+
Serve contract endpoints without a Nuxt build (frontend development against a
|
|
253
|
+
not-yet-implemented backend):
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
npx nuxt-api-contract mock contracts/index.ts --port 4000 --seed 42
|
|
257
|
+
# add --lenient to skip request validation, --delay 300 for artificial latency
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Or programmatically:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { startMockServer } from 'nuxt-api-contract/mock'
|
|
264
|
+
|
|
265
|
+
const mock = await startMockServer({ contracts: [GetUser, ListUsers], port: 4000 })
|
|
266
|
+
// GET /__mock/contracts lists available endpoints; CORS is enabled.
|
|
267
|
+
await mock.close()
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Responses are deterministic for a given `--seed`; password/token fields are
|
|
271
|
+
always masked.
|
|
247
272
|
|
|
248
273
|
## Testing
|
|
249
274
|
|
|
@@ -322,6 +347,7 @@ and DevTools are not part of any runtime import chain (importing
|
|
|
322
347
|
| `nuxt-api-contract/server` | `defineContractHandler`, validation helpers |
|
|
323
348
|
| `nuxt-api-contract/testing` | `callContract` |
|
|
324
349
|
| `nuxt-api-contract/openapi` | OpenAPI generator |
|
|
350
|
+
| `nuxt-api-contract/mock` | Standalone mock server + mock generators |
|
|
325
351
|
| `nuxt-api-contract/shared` | Shared primitives |
|
|
326
352
|
|
|
327
353
|
## Limitations
|
|
@@ -330,15 +356,14 @@ and DevTools are not part of any runtime import chain (importing
|
|
|
330
356
|
- Request bodies are JSON; `multipart/form-data` (file uploads) is planned —
|
|
331
357
|
the contract abstraction already does not assume JSON-only bodies.
|
|
332
358
|
- OpenAPI conversion is best-effort for `transform` / `refine` / `preprocess`.
|
|
333
|
-
- The standalone mock server (`nuxt-api-contract mock`) is not implemented yet.
|
|
334
359
|
- Auto-discovery is directory-based (`contracts/`, `server/contracts/`) rather
|
|
335
360
|
than a build-time scanner.
|
|
336
361
|
|
|
337
362
|
## Roadmap
|
|
338
363
|
|
|
339
|
-
See [ROADMAP.md](ROADMAP.md). In short: **0.1.0
|
|
340
|
-
|
|
341
|
-
|
|
364
|
+
See [ROADMAP.md](ROADMAP.md). In short: **0.1.0** core contracts + OpenAPI +
|
|
365
|
+
DevTools and **0.2.0** standalone mock server / generated mocks are released;
|
|
366
|
+
next: 0.3.0 extended contract testing, 0.4.0 OpenAPI client generation,
|
|
342
367
|
0.5.0 external API contracts, 0.6.0 contract versioning, 1.0.0 stable API.
|
|
343
368
|
|
|
344
369
|
## Development
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { B as BUILT_IN_ERROR_CODES, A as ApiError, s as serializeApiError } from '../shared/nuxt-api-contract.obS6uV8A.mjs';
|
|
3
|
+
import { v as validateContractInput, a as validateContractResponse } from '../shared/nuxt-api-contract.DDpgZj2g.mjs';
|
|
4
|
+
import { g as generateMockResponse } from '../shared/nuxt-api-contract.Bd2Y7Lx0.mjs';
|
|
5
|
+
import 'zod';
|
|
6
|
+
import '../shared/nuxt-api-contract.DCAU2j7t.mjs';
|
|
7
|
+
import '../shared/nuxt-api-contract.V15soI_f.mjs';
|
|
8
|
+
|
|
9
|
+
function pathToRegex(path) {
|
|
10
|
+
const names = [];
|
|
11
|
+
const source = path.split("/").map((segment) => {
|
|
12
|
+
const match = /^:([A-Za-z_][A-Za-z0-9_]*)$/.exec(segment);
|
|
13
|
+
if (!match) return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
+
names.push(match[1]);
|
|
15
|
+
return `(?<${match[1]}>[^/]+)`;
|
|
16
|
+
}).join("/");
|
|
17
|
+
return { regex: new RegExp(`^${source}/?$`), names };
|
|
18
|
+
}
|
|
19
|
+
function corsHeaders() {
|
|
20
|
+
return {
|
|
21
|
+
"access-control-allow-origin": "*",
|
|
22
|
+
"access-control-allow-methods": "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS",
|
|
23
|
+
"access-control-allow-headers": "content-type, authorization, x-requested-with"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function send(res, status, payload, extra = {}) {
|
|
27
|
+
const body = JSON.stringify(payload);
|
|
28
|
+
res.writeHead(status, {
|
|
29
|
+
"content-type": "application/json; charset=utf-8",
|
|
30
|
+
"x-mock-server": "nuxt-api-contract",
|
|
31
|
+
...corsHeaders(),
|
|
32
|
+
...extra
|
|
33
|
+
});
|
|
34
|
+
res.end(body);
|
|
35
|
+
}
|
|
36
|
+
function sleep(ms) {
|
|
37
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
|
+
}
|
|
39
|
+
function buildMockMatchers(contracts) {
|
|
40
|
+
return contracts.map((contract) => {
|
|
41
|
+
const { regex, names } = pathToRegex(contract.path);
|
|
42
|
+
return { contract, method: contract.method, regex, names };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function readJsonBody(req) {
|
|
46
|
+
const chunks = [];
|
|
47
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
48
|
+
if (chunks.length === 0) return { ok: true, value: void 0 };
|
|
49
|
+
try {
|
|
50
|
+
return { ok: true, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) };
|
|
51
|
+
} catch {
|
|
52
|
+
return { ok: false, value: void 0 };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function errorPayload(error) {
|
|
56
|
+
return serializeApiError(error);
|
|
57
|
+
}
|
|
58
|
+
function createMockServer(options) {
|
|
59
|
+
const matchers = buildMockMatchers(options.contracts);
|
|
60
|
+
return createServer(async (req, res) => {
|
|
61
|
+
const started = Date.now();
|
|
62
|
+
try {
|
|
63
|
+
if (req.method === "OPTIONS") {
|
|
64
|
+
res.writeHead(204, corsHeaders());
|
|
65
|
+
res.end();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
69
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
70
|
+
const pathMatch = matchers.find((matcher) => matcher.method === method && matcher.regex.test(url.pathname)) ?? matchers.find((matcher) => matcher.regex.test(url.pathname));
|
|
71
|
+
if (url.pathname === "/__mock/contracts" || url.pathname === "/__mock/contracts/") {
|
|
72
|
+
send(res, 200, {
|
|
73
|
+
contracts: options.contracts.map((contract2) => ({
|
|
74
|
+
method: contract2.method,
|
|
75
|
+
path: contract2.path,
|
|
76
|
+
name: contract2.name ?? null,
|
|
77
|
+
tags: contract2.tags ?? []
|
|
78
|
+
}))
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!pathMatch) {
|
|
83
|
+
send(res, 404, { error: { code: BUILT_IN_ERROR_CODES.notFound, message: `No mock for ${method} ${url.pathname}` } });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (pathMatch.method !== method) {
|
|
87
|
+
send(res, 405, { error: { code: BUILT_IN_ERROR_CODES.methodNotAllowed, message: `${pathMatch.method} ${pathMatch.contract.path} does not accept ${method}` } });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const match = pathMatch;
|
|
91
|
+
const contract = match.contract;
|
|
92
|
+
const params = {};
|
|
93
|
+
const exec = match.regex.exec(url.pathname);
|
|
94
|
+
for (const name of match.names) {
|
|
95
|
+
params[name] = decodeURIComponent(exec?.groups?.[name] ?? "");
|
|
96
|
+
}
|
|
97
|
+
const query = {};
|
|
98
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
99
|
+
query[key] = value;
|
|
100
|
+
}
|
|
101
|
+
let body;
|
|
102
|
+
let bodyError;
|
|
103
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
104
|
+
const parsed = await readJsonBody(req);
|
|
105
|
+
if (!parsed.ok) {
|
|
106
|
+
bodyError = new ApiError({ code: BUILT_IN_ERROR_CODES.validation, message: "Request body must be valid JSON", statusCode: 400 });
|
|
107
|
+
}
|
|
108
|
+
body = parsed.value;
|
|
109
|
+
}
|
|
110
|
+
if (options.delay) await sleep(options.delay);
|
|
111
|
+
if (!options.lenient) {
|
|
112
|
+
try {
|
|
113
|
+
if (bodyError) throw bodyError;
|
|
114
|
+
if (contract.params) validateContractInput(contract, "params", contract.params, params);
|
|
115
|
+
if (contract.query) validateContractInput(contract, "query", contract.query, query);
|
|
116
|
+
if (method !== "GET" && method !== "HEAD" && contract.body) {
|
|
117
|
+
validateContractInput(contract, "body", contract.body, body);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const apiError = error instanceof ApiError ? error : new ApiError({ code: BUILT_IN_ERROR_CODES.internal, message: String(error) });
|
|
121
|
+
send(res, apiError.statusCode, errorPayload(apiError));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let response;
|
|
126
|
+
try {
|
|
127
|
+
response = generateMockResponse(contract, { seed: options.seed });
|
|
128
|
+
if (contract.response) {
|
|
129
|
+
validateContractResponse(contract, contract.response, response);
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
send(res, 500, { error: { code: BUILT_IN_ERROR_CODES.internal, message: error instanceof Error ? error.message : String(error) } });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
send(res, 200, response, { "x-mock-time": String(Date.now() - started) });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
send(res, 500, { error: { code: BUILT_IN_ERROR_CODES.internal, message: error instanceof Error ? error.message : String(error) } });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function startMockServer(options) {
|
|
142
|
+
const server = createMockServer(options);
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
server.once("error", reject);
|
|
145
|
+
server.listen(options.port ?? 4e3, options.host ?? "127.0.0.1", () => {
|
|
146
|
+
const address = server.address();
|
|
147
|
+
const port = typeof address === "object" && address !== null ? address.port : options.port ?? 4e3;
|
|
148
|
+
const host = options.host ?? "127.0.0.1";
|
|
149
|
+
resolve({
|
|
150
|
+
server,
|
|
151
|
+
port,
|
|
152
|
+
host,
|
|
153
|
+
url: `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`,
|
|
154
|
+
close: () => new Promise((resolveClose, rejectClose) => {
|
|
155
|
+
server.close((error) => error ? rejectClose(error) : resolveClose());
|
|
156
|
+
})
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { buildMockMatchers, createMockServer, startMockServer };
|
package/dist/cli.mjs
CHANGED
|
@@ -69,7 +69,39 @@ async function main() {
|
|
|
69
69
|
console.log(`[nuxt-api-contract] OpenAPI document with ${contracts.length} contract(s) written to ${output}`);
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
if (command === "mock") {
|
|
73
|
+
const entry = positional[0];
|
|
74
|
+
if (!entry || !existsSync(entry)) {
|
|
75
|
+
console.error("[nuxt-api-contract] Usage: nuxt-api-contract mock <entry> [--port 4000] [--seed 42] [--lenient]");
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const contracts = await loadContractsFromEntry(resolve(entry));
|
|
80
|
+
const { startMockServer } = await import('./chunks/server.mjs');
|
|
81
|
+
const seed = flags.seed !== void 0 && flags.seed !== true ? Number(flags.seed) : void 0;
|
|
82
|
+
const handle = await startMockServer({
|
|
83
|
+
contracts,
|
|
84
|
+
port: flags.port !== void 0 && flags.port !== true ? Number(flags.port) : 4e3,
|
|
85
|
+
host: typeof flags.host === "string" ? flags.host : "127.0.0.1",
|
|
86
|
+
seed,
|
|
87
|
+
delay: flags.delay !== void 0 && flags.delay !== true ? Number(flags.delay) : void 0,
|
|
88
|
+
lenient: flags.lenient === true
|
|
89
|
+
});
|
|
90
|
+
console.log(`[nuxt-api-contract] Mock server listening on ${handle.url} (seed: ${seed ?? "randomized per contract"}, lenient: ${flags.lenient === true})`);
|
|
91
|
+
for (const contract of contracts) {
|
|
92
|
+
console.log(` ${contract.method.padEnd(6)} ${handle.url}${contract.path}`);
|
|
93
|
+
}
|
|
94
|
+
console.log(" GET /__mock/contracts (list endpoints)");
|
|
95
|
+
console.log("Press Ctrl+C to stop.");
|
|
96
|
+
const shutdown = async () => {
|
|
97
|
+
await handle.close();
|
|
98
|
+
process.exit(0);
|
|
99
|
+
};
|
|
100
|
+
process.on("SIGINT", shutdown);
|
|
101
|
+
process.on("SIGTERM", shutdown);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
console.error(`[nuxt-api-contract] Unknown command "${command ?? ""}". Available commands: openapi, mock`);
|
|
73
105
|
process.exitCode = 1;
|
|
74
106
|
}
|
|
75
107
|
function toMinimalYaml(value, indent = 0) {
|
package/dist/client.d.mts
CHANGED
|
@@ -1,70 +1,6 @@
|
|
|
1
|
+
export { C as ContractMock, M as MockResponseInput, b as buildRequestPath, c as clearContractRegistry, d as defineApiContract, g as getContractByName, a as getContractMock, i as isApiContract, l as listRegisteredContracts, m as mockContract, r as registerContract } from './shared/nuxt-api-contract.BgPd-YXc.mjs';
|
|
1
2
|
export { A as ApiError, a as ApiErrorPayload, B as BUILT_IN_ERROR_CODES, C as CreateApiErrorInput, V as ValidationIssue, c as createApiError, f as formatValidationMessage, i as isApiError, p as parseApiErrorPayload, s as sanitizeIssues, b as serializeApiError, t as toApiError, d as toValidationIssues } from './shared/nuxt-api-contract.D31EDcwH.mjs';
|
|
2
|
-
|
|
3
|
-
export {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Registers a named contract. Called automatically by `defineApiContract`
|
|
8
|
-
* when a `name` is provided. Duplicate names are overwritten with a warning.
|
|
9
|
-
*/
|
|
10
|
-
declare function registerContract(contract: AnyApiContract): void;
|
|
11
|
-
declare function getContractByName(name: string): AnyApiContract | undefined;
|
|
12
|
-
declare function listRegisteredContracts(): AnyApiContract[];
|
|
13
|
-
/** Test helper: clears the registry. */
|
|
14
|
-
declare function clearContractRegistry(): void;
|
|
15
|
-
interface ContractMock<C extends AnyApiContract> {
|
|
16
|
-
/** Factory producing a (validated) response for the contract. */
|
|
17
|
-
response?: () => MaybePromise<MockResponseInput<C>>;
|
|
18
|
-
/** Simulated latency in ms. */
|
|
19
|
-
delay?: number;
|
|
20
|
-
}
|
|
21
|
-
type MockResponseInput<C extends AnyApiContract> = C['response'] extends zod.ZodType ? ContractHandlerResponse<C> : unknown;
|
|
22
|
-
/**
|
|
23
|
-
* Registers a mock implementation for a contract. When the module option
|
|
24
|
-
* `apiContract.mocks` is enabled, contract handlers return the mock response
|
|
25
|
-
* (still validated against the response schema).
|
|
26
|
-
*/
|
|
27
|
-
declare function mockContract<C extends AnyApiContract>(contract: C, mock: ContractMock<C>): void;
|
|
28
|
-
declare function getContractMock<C extends AnyApiContract>(contract: C): ContractMock<C> | undefined;
|
|
29
|
-
/**
|
|
30
|
-
* Defines a type-safe API contract.
|
|
31
|
-
*
|
|
32
|
-
* The contract is the single source of truth for runtime validation,
|
|
33
|
-
* TypeScript types, OpenAPI generation, DevTools and mocks.
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* ```ts
|
|
37
|
-
* export const GetUser = defineApiContract({
|
|
38
|
-
* method: 'GET',
|
|
39
|
-
* path: '/api/users/:id',
|
|
40
|
-
* params: z.object({ id: z.string().uuid() }),
|
|
41
|
-
* response: z.object({ id: z.string(), name: z.string() }),
|
|
42
|
-
* })
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
declare function defineApiContract<const TDef extends ApiContractDefinition>(definition: TDef): ContractFromDefinition<TDef>;
|
|
46
|
-
/** Type guard for contract objects. */
|
|
47
|
-
declare function isApiContract(value: unknown): value is AnyApiContract;
|
|
48
|
-
/**
|
|
49
|
-
* Builds the request URL from a contract path and concrete params.
|
|
50
|
-
* Remaining params that are not part of the path are ignored.
|
|
51
|
-
*/
|
|
52
|
-
declare function buildRequestPath(path: string, params: Record<string, unknown> | undefined): string;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Serialization helpers shared by the client transport and cache keys.
|
|
56
|
-
*
|
|
57
|
-
* The protocol intentionally stays minimal: standard JSON plus two common
|
|
58
|
-
* edge cases (Date and bigint). Response serialization relies on Nitro's
|
|
59
|
-
* built-in devalue support, which already handles Date, RegExp, etc.
|
|
60
|
-
*/
|
|
61
|
-
/** Converts query values into URL-safe primitives (Date -> ISO, bigint -> string). */
|
|
62
|
-
type SerializedQueryValue = string | number | boolean | Array<string | number | boolean>;
|
|
63
|
-
declare function serializeQueryValue(value: unknown): SerializedQueryValue;
|
|
64
|
-
/** Prepares a query object for `$fetch` / URL building; drops `undefined` entries. */
|
|
65
|
-
declare function serializeQuery(query: Record<string, unknown> | undefined): Record<string, SerializedQueryValue> | undefined;
|
|
66
|
-
/** Deterministic JSON stringify (sorted keys) for stable SSR cache keys. */
|
|
67
|
-
declare function stableStringify(value: unknown): string;
|
|
68
|
-
|
|
69
|
-
export { AnyApiContract, ApiContractDefinition, ContractFromDefinition, ContractHandlerResponse, MaybePromise, buildRequestPath, clearContractRegistry, defineApiContract, getContractByName, getContractMock, isApiContract, listRegisteredContracts, mockContract, registerContract, serializeQuery, serializeQueryValue, stableStringify };
|
|
70
|
-
export type { ContractMock, MockResponseInput };
|
|
3
|
+
export { s as serializeQuery, a as serializeQueryValue, b as stableStringify } from './shared/nuxt-api-contract.DZfOzVaB.mjs';
|
|
4
|
+
export { A as API_CONTRACT_KIND, a as AnyApiContract, b as ApiContract, c as ApiContractDefinition, d as ApiRequestOptions, e as AuthConfig, C as ContractBodyInput, f as ContractClientResponse, g as ContractErrorCode, h as ContractFromDefinition, i as ContractHandlerResponse, j as ContractHeadersInput, k as ContractParamsInput, l as ContractPathParams, m as ContractQueryInput, E as EmptyObject, n as ExtractSchemaInput, o as ExtractSchemaOutput, H as HttpMethod, I as IsEmptyObject, M as MaybePromise, P as PathParams, R as ResolvedApiRequestOptions, S as SplitPath } from './shared/nuxt-api-contract.CFG8gzJH.mjs';
|
|
5
|
+
export { M as MockGenerateOptions, a as autoMockContract, g as generateMockResponse } from './shared/nuxt-api-contract.BOxyDvRy.mjs';
|
|
6
|
+
import 'zod';
|
package/dist/client.d.ts
CHANGED
|
@@ -1,70 +1,6 @@
|
|
|
1
|
+
export { C as ContractMock, M as MockResponseInput, b as buildRequestPath, c as clearContractRegistry, d as defineApiContract, g as getContractByName, a as getContractMock, i as isApiContract, l as listRegisteredContracts, m as mockContract, r as registerContract } from './shared/nuxt-api-contract.C7KxMQHa.js';
|
|
1
2
|
export { A as ApiError, a as ApiErrorPayload, B as BUILT_IN_ERROR_CODES, C as CreateApiErrorInput, V as ValidationIssue, c as createApiError, f as formatValidationMessage, i as isApiError, p as parseApiErrorPayload, s as sanitizeIssues, b as serializeApiError, t as toApiError, d as toValidationIssues } from './shared/nuxt-api-contract.D31EDcwH.js';
|
|
2
|
-
|
|
3
|
-
export {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Registers a named contract. Called automatically by `defineApiContract`
|
|
8
|
-
* when a `name` is provided. Duplicate names are overwritten with a warning.
|
|
9
|
-
*/
|
|
10
|
-
declare function registerContract(contract: AnyApiContract): void;
|
|
11
|
-
declare function getContractByName(name: string): AnyApiContract | undefined;
|
|
12
|
-
declare function listRegisteredContracts(): AnyApiContract[];
|
|
13
|
-
/** Test helper: clears the registry. */
|
|
14
|
-
declare function clearContractRegistry(): void;
|
|
15
|
-
interface ContractMock<C extends AnyApiContract> {
|
|
16
|
-
/** Factory producing a (validated) response for the contract. */
|
|
17
|
-
response?: () => MaybePromise<MockResponseInput<C>>;
|
|
18
|
-
/** Simulated latency in ms. */
|
|
19
|
-
delay?: number;
|
|
20
|
-
}
|
|
21
|
-
type MockResponseInput<C extends AnyApiContract> = C['response'] extends zod.ZodType ? ContractHandlerResponse<C> : unknown;
|
|
22
|
-
/**
|
|
23
|
-
* Registers a mock implementation for a contract. When the module option
|
|
24
|
-
* `apiContract.mocks` is enabled, contract handlers return the mock response
|
|
25
|
-
* (still validated against the response schema).
|
|
26
|
-
*/
|
|
27
|
-
declare function mockContract<C extends AnyApiContract>(contract: C, mock: ContractMock<C>): void;
|
|
28
|
-
declare function getContractMock<C extends AnyApiContract>(contract: C): ContractMock<C> | undefined;
|
|
29
|
-
/**
|
|
30
|
-
* Defines a type-safe API contract.
|
|
31
|
-
*
|
|
32
|
-
* The contract is the single source of truth for runtime validation,
|
|
33
|
-
* TypeScript types, OpenAPI generation, DevTools and mocks.
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* ```ts
|
|
37
|
-
* export const GetUser = defineApiContract({
|
|
38
|
-
* method: 'GET',
|
|
39
|
-
* path: '/api/users/:id',
|
|
40
|
-
* params: z.object({ id: z.string().uuid() }),
|
|
41
|
-
* response: z.object({ id: z.string(), name: z.string() }),
|
|
42
|
-
* })
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
declare function defineApiContract<const TDef extends ApiContractDefinition>(definition: TDef): ContractFromDefinition<TDef>;
|
|
46
|
-
/** Type guard for contract objects. */
|
|
47
|
-
declare function isApiContract(value: unknown): value is AnyApiContract;
|
|
48
|
-
/**
|
|
49
|
-
* Builds the request URL from a contract path and concrete params.
|
|
50
|
-
* Remaining params that are not part of the path are ignored.
|
|
51
|
-
*/
|
|
52
|
-
declare function buildRequestPath(path: string, params: Record<string, unknown> | undefined): string;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Serialization helpers shared by the client transport and cache keys.
|
|
56
|
-
*
|
|
57
|
-
* The protocol intentionally stays minimal: standard JSON plus two common
|
|
58
|
-
* edge cases (Date and bigint). Response serialization relies on Nitro's
|
|
59
|
-
* built-in devalue support, which already handles Date, RegExp, etc.
|
|
60
|
-
*/
|
|
61
|
-
/** Converts query values into URL-safe primitives (Date -> ISO, bigint -> string). */
|
|
62
|
-
type SerializedQueryValue = string | number | boolean | Array<string | number | boolean>;
|
|
63
|
-
declare function serializeQueryValue(value: unknown): SerializedQueryValue;
|
|
64
|
-
/** Prepares a query object for `$fetch` / URL building; drops `undefined` entries. */
|
|
65
|
-
declare function serializeQuery(query: Record<string, unknown> | undefined): Record<string, SerializedQueryValue> | undefined;
|
|
66
|
-
/** Deterministic JSON stringify (sorted keys) for stable SSR cache keys. */
|
|
67
|
-
declare function stableStringify(value: unknown): string;
|
|
68
|
-
|
|
69
|
-
export { AnyApiContract, ApiContractDefinition, ContractFromDefinition, ContractHandlerResponse, MaybePromise, buildRequestPath, clearContractRegistry, defineApiContract, getContractByName, getContractMock, isApiContract, listRegisteredContracts, mockContract, registerContract, serializeQuery, serializeQueryValue, stableStringify };
|
|
70
|
-
export type { ContractMock, MockResponseInput };
|
|
3
|
+
export { s as serializeQuery, a as serializeQueryValue, b as stableStringify } from './shared/nuxt-api-contract.DZfOzVaB.js';
|
|
4
|
+
export { A as API_CONTRACT_KIND, a as AnyApiContract, b as ApiContract, c as ApiContractDefinition, d as ApiRequestOptions, e as AuthConfig, C as ContractBodyInput, f as ContractClientResponse, g as ContractErrorCode, h as ContractFromDefinition, i as ContractHandlerResponse, j as ContractHeadersInput, k as ContractParamsInput, l as ContractPathParams, m as ContractQueryInput, E as EmptyObject, n as ExtractSchemaInput, o as ExtractSchemaOutput, H as HttpMethod, I as IsEmptyObject, M as MaybePromise, P as PathParams, R as ResolvedApiRequestOptions, S as SplitPath } from './shared/nuxt-api-contract.CFG8gzJH.js';
|
|
5
|
+
export { M as MockGenerateOptions, a as autoMockContract, g as generateMockResponse } from './shared/nuxt-api-contract.BuWjAHJe.js';
|
|
6
|
+
import 'zod';
|
package/dist/client.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export { b as buildRequestPath, c as clearContractRegistry, d as defineApiContract, g as getContractByName, a as getContractMock, i as isApiContract, l as listRegisteredContracts, m as mockContract, r as registerContract
|
|
1
|
+
export { b as buildRequestPath, c as clearContractRegistry, d as defineApiContract, g as getContractByName, a as getContractMock, i as isApiContract, l as listRegisteredContracts, m as mockContract, r as registerContract } from './shared/nuxt-api-contract.V15soI_f.mjs';
|
|
2
2
|
export { A as ApiError, B as BUILT_IN_ERROR_CODES, c as createApiError, i as isApiError, p as parseApiErrorPayload, s as serializeApiError, t as toApiError } from './shared/nuxt-api-contract.obS6uV8A.mjs';
|
|
3
3
|
export { f as formatValidationMessage, s as sanitizeIssues, t as toValidationIssues } from './shared/nuxt-api-contract.DCAU2j7t.mjs';
|
|
4
|
+
export { s as serializeQuery, a as serializeQueryValue, b as stableStringify } from './shared/nuxt-api-contract.Dr4tPqGB.mjs';
|
|
5
|
+
export { a as autoMockContract, g as generateMockResponse } from './shared/nuxt-api-contract.Bd2Y7Lx0.mjs';
|
package/dist/composables.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useAsyncData, useNuxtApp, useRequestEvent } from '#imports';
|
|
2
|
-
import {
|
|
2
|
+
import { b as buildRequestPath } from './shared/nuxt-api-contract.V15soI_f.mjs';
|
|
3
3
|
import { p as parseApiErrorPayload, A as ApiError, t as toApiError } from './shared/nuxt-api-contract.obS6uV8A.mjs';
|
|
4
|
+
import { b as stableStringify, s as serializeQuery } from './shared/nuxt-api-contract.Dr4tPqGB.mjs';
|
|
4
5
|
|
|
5
6
|
function createRequestKey(contract, options) {
|
|
6
7
|
const { params, query, body, extraHeaders, ...rest } = options ?? {};
|
package/dist/mock.d.mts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export { M as MockGenerateOptions, a as autoMockContract, c as createRng, g as generateMockResponse, b as generateMockValue } from './shared/nuxt-api-contract.BOxyDvRy.mjs';
|
|
2
|
+
import { Server } from 'node:http';
|
|
3
|
+
import { AnyApiContract } from './client.mjs';
|
|
4
|
+
import 'zod';
|
|
5
|
+
import './shared/nuxt-api-contract.BgPd-YXc.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Standalone mock server: serves contract-defined endpoints with
|
|
9
|
+
* deterministic generated responses. Zero framework dependencies — plain
|
|
10
|
+
* `node:http`, so it can run outside Nuxt:
|
|
11
|
+
*
|
|
12
|
+
* ```bash
|
|
13
|
+
* npx nuxt-api-contract mock contracts/index.ts --port 4000 --seed 42
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface MockServerOptions {
|
|
18
|
+
contracts: AnyApiContract[];
|
|
19
|
+
/** Listen port (default: 4000; `0` picks a random free port). */
|
|
20
|
+
port?: number;
|
|
21
|
+
/** Listen host (default: `127.0.0.1`). */
|
|
22
|
+
host?: string;
|
|
23
|
+
/** Deterministic generation seed. */
|
|
24
|
+
seed?: number;
|
|
25
|
+
/** Artificial latency in ms per request. */
|
|
26
|
+
delay?: number;
|
|
27
|
+
/** Return generated responses even for invalid requests (default: false). */
|
|
28
|
+
lenient?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface MockServerHandle {
|
|
31
|
+
server: Server;
|
|
32
|
+
port: number;
|
|
33
|
+
host: string;
|
|
34
|
+
url: string;
|
|
35
|
+
close: () => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
interface Matcher {
|
|
38
|
+
contract: AnyApiContract;
|
|
39
|
+
method: string;
|
|
40
|
+
regex: RegExp;
|
|
41
|
+
names: string[];
|
|
42
|
+
}
|
|
43
|
+
/** Builds an in-memory matcher index over the contracts. */
|
|
44
|
+
declare function buildMockMatchers(contracts: AnyApiContract[]): Matcher[];
|
|
45
|
+
/**
|
|
46
|
+
* Creates (without listening) a mock `node:http` server for the contracts.
|
|
47
|
+
* Prefer `startMockServer` for CLI / test usage.
|
|
48
|
+
*/
|
|
49
|
+
declare function createMockServer(options: MockServerOptions): Server;
|
|
50
|
+
/** Creates the mock server and starts listening. Resolves once ready. */
|
|
51
|
+
declare function startMockServer(options: MockServerOptions): Promise<MockServerHandle>;
|
|
52
|
+
|
|
53
|
+
export { buildMockMatchers, createMockServer, startMockServer };
|
|
54
|
+
export type { MockServerHandle, MockServerOptions };
|
package/dist/mock.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export { M as MockGenerateOptions, a as autoMockContract, c as createRng, g as generateMockResponse, b as generateMockValue } from './shared/nuxt-api-contract.BuWjAHJe.js';
|
|
2
|
+
import { Server } from 'node:http';
|
|
3
|
+
import { AnyApiContract } from './client.js';
|
|
4
|
+
import 'zod';
|
|
5
|
+
import './shared/nuxt-api-contract.C7KxMQHa.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Standalone mock server: serves contract-defined endpoints with
|
|
9
|
+
* deterministic generated responses. Zero framework dependencies — plain
|
|
10
|
+
* `node:http`, so it can run outside Nuxt:
|
|
11
|
+
*
|
|
12
|
+
* ```bash
|
|
13
|
+
* npx nuxt-api-contract mock contracts/index.ts --port 4000 --seed 42
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface MockServerOptions {
|
|
18
|
+
contracts: AnyApiContract[];
|
|
19
|
+
/** Listen port (default: 4000; `0` picks a random free port). */
|
|
20
|
+
port?: number;
|
|
21
|
+
/** Listen host (default: `127.0.0.1`). */
|
|
22
|
+
host?: string;
|
|
23
|
+
/** Deterministic generation seed. */
|
|
24
|
+
seed?: number;
|
|
25
|
+
/** Artificial latency in ms per request. */
|
|
26
|
+
delay?: number;
|
|
27
|
+
/** Return generated responses even for invalid requests (default: false). */
|
|
28
|
+
lenient?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface MockServerHandle {
|
|
31
|
+
server: Server;
|
|
32
|
+
port: number;
|
|
33
|
+
host: string;
|
|
34
|
+
url: string;
|
|
35
|
+
close: () => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
interface Matcher {
|
|
38
|
+
contract: AnyApiContract;
|
|
39
|
+
method: string;
|
|
40
|
+
regex: RegExp;
|
|
41
|
+
names: string[];
|
|
42
|
+
}
|
|
43
|
+
/** Builds an in-memory matcher index over the contracts. */
|
|
44
|
+
declare function buildMockMatchers(contracts: AnyApiContract[]): Matcher[];
|
|
45
|
+
/**
|
|
46
|
+
* Creates (without listening) a mock `node:http` server for the contracts.
|
|
47
|
+
* Prefer `startMockServer` for CLI / test usage.
|
|
48
|
+
*/
|
|
49
|
+
declare function createMockServer(options: MockServerOptions): Server;
|
|
50
|
+
/** Creates the mock server and starts listening. Resolves once ready. */
|
|
51
|
+
declare function startMockServer(options: MockServerOptions): Promise<MockServerHandle>;
|
|
52
|
+
|
|
53
|
+
export { buildMockMatchers, createMockServer, startMockServer };
|
|
54
|
+
export type { MockServerHandle, MockServerOptions };
|
package/dist/mock.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { a as autoMockContract, c as createRng, g as generateMockResponse, b as generateMockValue } from './shared/nuxt-api-contract.Bd2Y7Lx0.mjs';
|
|
2
|
+
export { buildMockMatchers, createMockServer, startMockServer } from './chunks/server.mjs';
|
|
3
|
+
import './shared/nuxt-api-contract.V15soI_f.mjs';
|
|
4
|
+
import 'node:http';
|
|
5
|
+
import './shared/nuxt-api-contract.obS6uV8A.mjs';
|
|
6
|
+
import './shared/nuxt-api-contract.DDpgZj2g.mjs';
|
|
7
|
+
import 'zod';
|
|
8
|
+
import './shared/nuxt-api-contract.DCAU2j7t.mjs';
|
package/dist/module.d.mts
CHANGED
|
@@ -20,8 +20,14 @@ interface ApiContractModuleOptions {
|
|
|
20
20
|
description?: string;
|
|
21
21
|
output?: string;
|
|
22
22
|
};
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Mock mode:
|
|
25
|
+
* - `false` (default): real handlers.
|
|
26
|
+
* - `true`: use mocks registered via `mockContract()`.
|
|
27
|
+
* - `'auto'`: use registered mocks, and generate mock responses
|
|
28
|
+
* from the response schemas for contracts without an explicit mock.
|
|
29
|
+
*/
|
|
30
|
+
mocks?: boolean | 'auto';
|
|
25
31
|
/** Nuxt DevTools panel (no-op when DevTools is not installed). */
|
|
26
32
|
devtools?: boolean;
|
|
27
33
|
/** Directories scanned for contract auto-imports. */
|
package/dist/module.d.ts
CHANGED
|
@@ -20,8 +20,14 @@ interface ApiContractModuleOptions {
|
|
|
20
20
|
description?: string;
|
|
21
21
|
output?: string;
|
|
22
22
|
};
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Mock mode:
|
|
25
|
+
* - `false` (default): real handlers.
|
|
26
|
+
* - `true`: use mocks registered via `mockContract()`.
|
|
27
|
+
* - `'auto'`: use registered mocks, and generate mock responses
|
|
28
|
+
* from the response schemas for contracts without an explicit mock.
|
|
29
|
+
*/
|
|
30
|
+
mocks?: boolean | 'auto';
|
|
25
31
|
/** Nuxt DevTools panel (no-op when DevTools is not installed). */
|
|
26
32
|
devtools?: boolean;
|
|
27
33
|
/** Directories scanned for contract auto-imports. */
|
package/dist/module.mjs
CHANGED
|
@@ -47,12 +47,14 @@ const module$1 = defineNuxtModule({
|
|
|
47
47
|
async setup(options, nuxt) {
|
|
48
48
|
const resolver = createResolver(import.meta.url);
|
|
49
49
|
const rootDir = nuxt.options.rootDir;
|
|
50
|
+
const mocksEnabled = options.mocks === true || options.mocks === "auto";
|
|
50
51
|
nuxt.options.runtimeConfig.apiContract = defu(nuxt.options.runtimeConfig.apiContract ?? {}, {
|
|
51
52
|
validateResponse: options.validateResponse ?? "development",
|
|
52
|
-
mocks:
|
|
53
|
+
mocks: mocksEnabled,
|
|
54
|
+
mocksAuto: options.mocks === "auto"
|
|
53
55
|
});
|
|
54
56
|
nuxt.options.runtimeConfig.public.apiContract = defu(nuxt.options.runtimeConfig.public.apiContract ?? {}, {
|
|
55
|
-
mocks:
|
|
57
|
+
mocks: mocksEnabled
|
|
56
58
|
});
|
|
57
59
|
addImports([
|
|
58
60
|
{ from: "nuxt-api-contract/client", name: "defineApiContract" },
|