nuxt-api-contract 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +385 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +99 -0
- package/dist/client.d.mts +70 -0
- package/dist/client.d.ts +70 -0
- package/dist/client.mjs +3 -0
- package/dist/composables.d.mts +45 -0
- package/dist/composables.d.ts +45 -0
- package/dist/composables.mjs +97 -0
- package/dist/module.d.mts +35 -0
- package/dist/module.d.ts +35 -0
- package/dist/module.mjs +145 -0
- package/dist/openapi.d.mts +46 -0
- package/dist/openapi.d.ts +46 -0
- package/dist/openapi.mjs +312 -0
- package/dist/runtime/server/devtoolsRoute.mjs +6 -0
- package/dist/runtime/server/openapiRoute.mjs +6 -0
- package/dist/runtime/shared/contract.mjs +77 -0
- package/dist/runtime/shared/errors.mjs +71 -0
- package/dist/runtime/shared/format.mjs +32 -0
- package/dist/runtime/shared/serialization.mjs +36 -0
- package/dist/runtime/shared/types.mjs +1 -0
- package/dist/server.d.mts +41 -0
- package/dist/server.d.ts +41 -0
- package/dist/server.mjs +94 -0
- package/dist/shared/nuxt-api-contract.B9JBCRk8.d.mts +37 -0
- package/dist/shared/nuxt-api-contract.CPm9WbWA.d.mts +165 -0
- package/dist/shared/nuxt-api-contract.CPm9WbWA.d.ts +165 -0
- package/dist/shared/nuxt-api-contract.D31EDcwH.d.mts +109 -0
- package/dist/shared/nuxt-api-contract.D31EDcwH.d.ts +109 -0
- package/dist/shared/nuxt-api-contract.DCAU2j7t.mjs +34 -0
- package/dist/shared/nuxt-api-contract.DDpgZj2g.mjs +66 -0
- package/dist/shared/nuxt-api-contract.QDGSGaVY.mjs +117 -0
- package/dist/shared/nuxt-api-contract.S1zqCiJX.d.ts +37 -0
- package/dist/shared/nuxt-api-contract.obS6uV8A.mjs +73 -0
- package/dist/shared.d.mts +4 -0
- package/dist/shared.d.ts +4 -0
- package/dist/shared.mjs +3 -0
- package/dist/testing.d.mts +55 -0
- package/dist/testing.d.ts +55 -0
- package/dist/testing.mjs +49 -0
- package/package.json +104 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { AsyncData } from '#app';
|
|
2
|
+
import { AnyApiContract, ApiRequestOptions, ContractClientResponse } from './client.mjs';
|
|
3
|
+
import { ApiError } from './client.mjs';
|
|
4
|
+
import 'zod';
|
|
5
|
+
|
|
6
|
+
type ContractFetch = (url: string, init: Record<string, unknown>) => Promise<unknown>;
|
|
7
|
+
interface ExecuteRequestContext {
|
|
8
|
+
/**
|
|
9
|
+
* Performs the actual fetch. Implemented per environment:
|
|
10
|
+
* - SSR: internal Nitro `event.$fetch` (no HTTP round-trip).
|
|
11
|
+
* - Browser: Nuxt `$fetch`.
|
|
12
|
+
*/
|
|
13
|
+
fetch: ContractFetch;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Executes a contract request and returns the typed response.
|
|
18
|
+
* Intended for actions, Pinia stores, background jobs and composables where
|
|
19
|
+
* a reactive `AsyncData` wrapper is not needed. Throws `ApiError` on failure.
|
|
20
|
+
*/
|
|
21
|
+
declare function useApiClient(): Promise<{
|
|
22
|
+
request: <C extends AnyApiContract>(contract: C, options?: ApiRequestOptions<C>) => Promise<ContractClientResponse<C>>;
|
|
23
|
+
tryRequest: <C extends AnyApiContract>(contract: C, options?: ApiRequestOptions<C>) => Promise<{
|
|
24
|
+
data: ContractClientResponse<C>;
|
|
25
|
+
error: ApiError | null;
|
|
26
|
+
}>;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Type-safe reactive API composable.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const { data, error, pending, refresh } = await useApi(GetUser, {
|
|
33
|
+
* params: { id: userId },
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* - `data` is typed from the contract's `response` schema.
|
|
38
|
+
* - Invalid `params` / `query` / `body` are TypeScript errors.
|
|
39
|
+
* - During SSR the request is routed through Nitro internally (no HTTP hop),
|
|
40
|
+
* the payload is transferred to the client, so there is no hydration mismatch.
|
|
41
|
+
*/
|
|
42
|
+
declare function useApi<C extends AnyApiContract>(contract: C, options?: ApiRequestOptions<C>): AsyncData<ContractClientResponse<C>, ApiError>;
|
|
43
|
+
|
|
44
|
+
export { useApi, useApiClient };
|
|
45
|
+
export type { ExecuteRequestContext };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { AsyncData } from '#app';
|
|
2
|
+
import { AnyApiContract, ApiRequestOptions, ContractClientResponse } from './client.js';
|
|
3
|
+
import { ApiError } from './client.js';
|
|
4
|
+
import 'zod';
|
|
5
|
+
|
|
6
|
+
type ContractFetch = (url: string, init: Record<string, unknown>) => Promise<unknown>;
|
|
7
|
+
interface ExecuteRequestContext {
|
|
8
|
+
/**
|
|
9
|
+
* Performs the actual fetch. Implemented per environment:
|
|
10
|
+
* - SSR: internal Nitro `event.$fetch` (no HTTP round-trip).
|
|
11
|
+
* - Browser: Nuxt `$fetch`.
|
|
12
|
+
*/
|
|
13
|
+
fetch: ContractFetch;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Executes a contract request and returns the typed response.
|
|
18
|
+
* Intended for actions, Pinia stores, background jobs and composables where
|
|
19
|
+
* a reactive `AsyncData` wrapper is not needed. Throws `ApiError` on failure.
|
|
20
|
+
*/
|
|
21
|
+
declare function useApiClient(): Promise<{
|
|
22
|
+
request: <C extends AnyApiContract>(contract: C, options?: ApiRequestOptions<C>) => Promise<ContractClientResponse<C>>;
|
|
23
|
+
tryRequest: <C extends AnyApiContract>(contract: C, options?: ApiRequestOptions<C>) => Promise<{
|
|
24
|
+
data: ContractClientResponse<C>;
|
|
25
|
+
error: ApiError | null;
|
|
26
|
+
}>;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Type-safe reactive API composable.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const { data, error, pending, refresh } = await useApi(GetUser, {
|
|
33
|
+
* params: { id: userId },
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* - `data` is typed from the contract's `response` schema.
|
|
38
|
+
* - Invalid `params` / `query` / `body` are TypeScript errors.
|
|
39
|
+
* - During SSR the request is routed through Nitro internally (no HTTP hop),
|
|
40
|
+
* the payload is transferred to the client, so there is no hydration mismatch.
|
|
41
|
+
*/
|
|
42
|
+
declare function useApi<C extends AnyApiContract>(contract: C, options?: ApiRequestOptions<C>): AsyncData<ContractClientResponse<C>, ApiError>;
|
|
43
|
+
|
|
44
|
+
export { useApi, useApiClient };
|
|
45
|
+
export type { ExecuteRequestContext };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useAsyncData, useNuxtApp, useRequestEvent } from '#imports';
|
|
2
|
+
import { f as stableStringify, b as buildRequestPath, s as serializeQuery } from './shared/nuxt-api-contract.QDGSGaVY.mjs';
|
|
3
|
+
import { p as parseApiErrorPayload, A as ApiError, t as toApiError } from './shared/nuxt-api-contract.obS6uV8A.mjs';
|
|
4
|
+
|
|
5
|
+
function createRequestKey(contract, options) {
|
|
6
|
+
const { params, query, body, extraHeaders, ...rest } = options ?? {};
|
|
7
|
+
return [
|
|
8
|
+
contract.name ?? contract.path,
|
|
9
|
+
contract.method,
|
|
10
|
+
contract.path,
|
|
11
|
+
stableStringify({ params, query, body, extraHeaders, rest })
|
|
12
|
+
].join("|");
|
|
13
|
+
}
|
|
14
|
+
function resolveRequestOptions(options) {
|
|
15
|
+
if (!options) return {};
|
|
16
|
+
return {
|
|
17
|
+
params: options.params,
|
|
18
|
+
query: serializeQuery(options.query),
|
|
19
|
+
body: options.body,
|
|
20
|
+
headers: { ...options.headers, ...options.extraHeaders },
|
|
21
|
+
signal: options.signal
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function toContractError(error) {
|
|
25
|
+
const fetchError = error;
|
|
26
|
+
const payload = fetchError && typeof fetchError === "object" ? parseApiErrorPayload(fetchError.data) : void 0;
|
|
27
|
+
if (payload) {
|
|
28
|
+
return new ApiError({
|
|
29
|
+
code: payload.code,
|
|
30
|
+
message: payload.message,
|
|
31
|
+
statusCode: payload.statusCode ?? 500,
|
|
32
|
+
details: payload.details,
|
|
33
|
+
issues: payload.issues
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const wrapped = toApiError(error);
|
|
37
|
+
return new ApiError({
|
|
38
|
+
code: wrapped.code,
|
|
39
|
+
message: error instanceof Error ? error.message : wrapped.message,
|
|
40
|
+
statusCode: error instanceof Error && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : 500
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function executeContractRequest(contract, options, doFetch) {
|
|
44
|
+
const resolved = resolveRequestOptions(options);
|
|
45
|
+
const url = buildRequestPath(contract.path, resolved.params);
|
|
46
|
+
const hasBodySchema = contract.body !== void 0;
|
|
47
|
+
const hasBody = resolved.body !== void 0 || hasBodySchema;
|
|
48
|
+
try {
|
|
49
|
+
const response = await doFetch(url, {
|
|
50
|
+
method: contract.method,
|
|
51
|
+
query: resolved.query,
|
|
52
|
+
body: contract.method === "GET" || contract.method === "HEAD" ? void 0 : hasBody ? resolved.body ?? {} : void 0,
|
|
53
|
+
headers: resolved.headers,
|
|
54
|
+
signal: resolved.signal
|
|
55
|
+
});
|
|
56
|
+
return response;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw toContractError(error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function useApiClient() {
|
|
63
|
+
const nuxtApp = useNuxtApp();
|
|
64
|
+
const event = import.meta.server ? useRequestEvent() : void 0;
|
|
65
|
+
const doFetch = (url, init) => {
|
|
66
|
+
if (event) {
|
|
67
|
+
const internalFetch = event.$fetch;
|
|
68
|
+
return internalFetch(url, init);
|
|
69
|
+
}
|
|
70
|
+
return nuxtApp.$fetch(url, init);
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
request: (contract, options) => executeContractRequest(contract, options, doFetch),
|
|
74
|
+
tryRequest: async (contract, options) => {
|
|
75
|
+
try {
|
|
76
|
+
const data = await executeContractRequest(contract, options, doFetch);
|
|
77
|
+
return { data, error: null };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return { data: void 0, error: toContractError(error) };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function useApi(contract, options) {
|
|
85
|
+
const key = createRequestKey(contract, options);
|
|
86
|
+
const result = useAsyncData(
|
|
87
|
+
key,
|
|
88
|
+
async () => {
|
|
89
|
+
const client = await useApiClient();
|
|
90
|
+
return client.request(contract, options);
|
|
91
|
+
},
|
|
92
|
+
{ deep: false, dedupe: "defer" }
|
|
93
|
+
);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { useApi, useApiClient };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ApiContractModuleOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Response validation mode:
|
|
6
|
+
* - `never`: skip response validation.
|
|
7
|
+
* - `development` (default): validate outside production builds.
|
|
8
|
+
* - `always`: validate in every environment.
|
|
9
|
+
*/
|
|
10
|
+
validateResponse?: 'never' | 'development' | 'always';
|
|
11
|
+
/** OpenAPI generation (build time) and serving. */
|
|
12
|
+
openapi?: {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
/** Route serving the generated document. */
|
|
15
|
+
path?: string;
|
|
16
|
+
/** Module exporting contracts (default array or named exports). */
|
|
17
|
+
entry?: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
version?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
output?: string;
|
|
22
|
+
};
|
|
23
|
+
/** Enable registered contract mocks (development/testing only). */
|
|
24
|
+
mocks?: boolean;
|
|
25
|
+
/** Nuxt DevTools panel (no-op when DevTools is not installed). */
|
|
26
|
+
devtools?: boolean;
|
|
27
|
+
/** Directories scanned for contract auto-imports. */
|
|
28
|
+
contractsDirs?: string[];
|
|
29
|
+
}
|
|
30
|
+
type ModuleOptions = ApiContractModuleOptions;
|
|
31
|
+
type ModuleHooks = Record<string, never>;
|
|
32
|
+
declare const _default: _nuxt_schema.NuxtModule<ApiContractModuleOptions, ApiContractModuleOptions, false>;
|
|
33
|
+
|
|
34
|
+
export { _default as default };
|
|
35
|
+
export type { ApiContractModuleOptions, ModuleHooks, ModuleOptions };
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ApiContractModuleOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Response validation mode:
|
|
6
|
+
* - `never`: skip response validation.
|
|
7
|
+
* - `development` (default): validate outside production builds.
|
|
8
|
+
* - `always`: validate in every environment.
|
|
9
|
+
*/
|
|
10
|
+
validateResponse?: 'never' | 'development' | 'always';
|
|
11
|
+
/** OpenAPI generation (build time) and serving. */
|
|
12
|
+
openapi?: {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
/** Route serving the generated document. */
|
|
15
|
+
path?: string;
|
|
16
|
+
/** Module exporting contracts (default array or named exports). */
|
|
17
|
+
entry?: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
version?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
output?: string;
|
|
22
|
+
};
|
|
23
|
+
/** Enable registered contract mocks (development/testing only). */
|
|
24
|
+
mocks?: boolean;
|
|
25
|
+
/** Nuxt DevTools panel (no-op when DevTools is not installed). */
|
|
26
|
+
devtools?: boolean;
|
|
27
|
+
/** Directories scanned for contract auto-imports. */
|
|
28
|
+
contractsDirs?: string[];
|
|
29
|
+
}
|
|
30
|
+
type ModuleOptions = ApiContractModuleOptions;
|
|
31
|
+
type ModuleHooks = Record<string, never>;
|
|
32
|
+
declare const _default: _nuxt_schema.NuxtModule<ApiContractModuleOptions, ApiContractModuleOptions, false>;
|
|
33
|
+
|
|
34
|
+
export { _default as default };
|
|
35
|
+
export type { ApiContractModuleOptions, ModuleHooks, ModuleOptions };
|
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, relative, isAbsolute } from 'node:path';
|
|
3
|
+
import { defineNuxtModule, createResolver, addImports, addServerHandler } from '@nuxt/kit';
|
|
4
|
+
import { defu } from 'defu';
|
|
5
|
+
import { createJiti } from 'jiti';
|
|
6
|
+
import { pickContracts, generateOpenApiDocument } from './openapi.mjs';
|
|
7
|
+
|
|
8
|
+
function contractRows(contracts) {
|
|
9
|
+
return contracts.map((contract) => {
|
|
10
|
+
const params = [...contract.path.matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g)].map((match) => match[1]);
|
|
11
|
+
return {
|
|
12
|
+
label: contract.name ?? "(anonymous)",
|
|
13
|
+
method: contract.method,
|
|
14
|
+
path: contract.path,
|
|
15
|
+
params,
|
|
16
|
+
tags: contract.tags ?? [],
|
|
17
|
+
errorCodes: contract.errors ? Object.keys(contract.errors) : []
|
|
18
|
+
};
|
|
19
|
+
}).sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
20
|
+
}
|
|
21
|
+
function buildDevtoolsHtml(contracts) {
|
|
22
|
+
const rows = contractRows(contracts);
|
|
23
|
+
return JSON.stringify(rows);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const CONFIG_KEY = "apiContract";
|
|
27
|
+
const OPENAPI_DEFAULTS = {
|
|
28
|
+
enabled: false,
|
|
29
|
+
path: "/_api-contracts/openapi.json",
|
|
30
|
+
entry: "contracts/index.ts"
|
|
31
|
+
};
|
|
32
|
+
const module$1 = defineNuxtModule({
|
|
33
|
+
meta: {
|
|
34
|
+
name: "nuxt-api-contract",
|
|
35
|
+
configKey: CONFIG_KEY,
|
|
36
|
+
compatibility: {
|
|
37
|
+
nuxt: ">=3.15.0"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
defaults: {
|
|
41
|
+
validateResponse: "development",
|
|
42
|
+
openapi: OPENAPI_DEFAULTS,
|
|
43
|
+
mocks: false,
|
|
44
|
+
devtools: true,
|
|
45
|
+
contractsDirs: ["contracts", "server/contracts"]
|
|
46
|
+
},
|
|
47
|
+
async setup(options, nuxt) {
|
|
48
|
+
const resolver = createResolver(import.meta.url);
|
|
49
|
+
const rootDir = nuxt.options.rootDir;
|
|
50
|
+
nuxt.options.runtimeConfig.apiContract = defu(nuxt.options.runtimeConfig.apiContract ?? {}, {
|
|
51
|
+
validateResponse: options.validateResponse ?? "development",
|
|
52
|
+
mocks: options.mocks ?? false
|
|
53
|
+
});
|
|
54
|
+
nuxt.options.runtimeConfig.public.apiContract = defu(nuxt.options.runtimeConfig.public.apiContract ?? {}, {
|
|
55
|
+
mocks: options.mocks ?? false
|
|
56
|
+
});
|
|
57
|
+
addImports([
|
|
58
|
+
{ from: "nuxt-api-contract/client", name: "defineApiContract" },
|
|
59
|
+
{ from: "nuxt-api-contract/client", name: "createApiError" },
|
|
60
|
+
{ from: "nuxt-api-contract/client", name: "isApiError" },
|
|
61
|
+
{ from: "nuxt-api-contract/client", name: "mockContract" },
|
|
62
|
+
{ from: "nuxt-api-contract/composables", name: "useApi" },
|
|
63
|
+
{ from: "nuxt-api-contract/composables", name: "useApiClient" },
|
|
64
|
+
{ from: "nuxt-api-contract/server", name: "defineContractHandler" }
|
|
65
|
+
]);
|
|
66
|
+
const hooks = nuxt.hooks;
|
|
67
|
+
hooks.hook("nitro:config", (nitroConfig) => {
|
|
68
|
+
nitroConfig.imports = defu(nitroConfig.imports ?? {}, {
|
|
69
|
+
presets: [
|
|
70
|
+
{
|
|
71
|
+
from: "nuxt-api-contract/server",
|
|
72
|
+
imports: ["defineContractHandler", "createApiError", "defineApiContract"]
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
for (const dir of options.contractsDirs ?? []) {
|
|
78
|
+
const absolute = resolve(rootDir, dir);
|
|
79
|
+
if (existsSync(absolute)) {
|
|
80
|
+
nuxt.options.imports.dirs = [...nuxt.options.imports.dirs ?? [], relative(nuxt.options.rootDir, absolute).replaceAll("\\", "/")];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const openapi = { ...OPENAPI_DEFAULTS, ...options.openapi ?? {} };
|
|
84
|
+
let contracts = [];
|
|
85
|
+
const openapiEnabled = openapi.enabled === true;
|
|
86
|
+
if (openapiEnabled) {
|
|
87
|
+
const entryPath = isAbsolute(openapi.entry) ? openapi.entry : resolve(rootDir, openapi.entry);
|
|
88
|
+
if (!existsSync(entryPath)) {
|
|
89
|
+
console.warn(`[nuxt-api-contract] OpenAPI entry "${openapi.entry}" not found; generation skipped.`);
|
|
90
|
+
} else {
|
|
91
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
92
|
+
const loaded = await jiti.import(entryPath);
|
|
93
|
+
const values = Array.isArray(loaded) ? [...loaded] : loaded !== null && typeof loaded === "object" ? Object.values(loaded) : [];
|
|
94
|
+
contracts = pickContracts(values);
|
|
95
|
+
const { document, warnings } = generateOpenApiDocument(contracts, {
|
|
96
|
+
title: openapi.title,
|
|
97
|
+
version: openapi.version,
|
|
98
|
+
description: openapi.description
|
|
99
|
+
});
|
|
100
|
+
for (const warning of warnings) {
|
|
101
|
+
console.warn(`[nuxt-api-contract] OpenAPI warning (${warning.contract}): ${warning.message}`);
|
|
102
|
+
}
|
|
103
|
+
const buildDir = resolve(nuxt.options.buildDir, "api-contracts");
|
|
104
|
+
mkdirSync(buildDir, { recursive: true });
|
|
105
|
+
const outputPath = resolve(buildDir, "openapi.mjs");
|
|
106
|
+
writeFileSync(outputPath, `export const document = ${JSON.stringify(document)}
|
|
107
|
+
`, "utf8");
|
|
108
|
+
nuxt.options.alias["#api-contracts-openapi"] = outputPath;
|
|
109
|
+
addServerHandler({
|
|
110
|
+
route: openapi.path,
|
|
111
|
+
handler: resolver.resolve("./runtime/server/openapiRoute")
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (options.devtools && nuxt.options.dev) {
|
|
116
|
+
const buildDir = resolve(nuxt.options.buildDir, "api-contracts");
|
|
117
|
+
mkdirSync(buildDir, { recursive: true });
|
|
118
|
+
const htmlPath = resolve(buildDir, "devtools.mjs");
|
|
119
|
+
writeFileSync(htmlPath, `export const html = ${JSON.stringify(buildDevtoolsHtml(contracts))}
|
|
120
|
+
`, "utf8");
|
|
121
|
+
nuxt.options.alias["#api-contracts-devtools"] = htmlPath;
|
|
122
|
+
const devtoolsRoute = "/_api-contracts";
|
|
123
|
+
addServerHandler({
|
|
124
|
+
route: devtoolsRoute,
|
|
125
|
+
handler: resolver.resolve("./runtime/server/devtoolsRoute")
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
const devtoolsKitModule = "@nuxt/devtools-kit";
|
|
129
|
+
const { addCustomTab } = await import(
|
|
130
|
+
/* @vite-ignore */
|
|
131
|
+
devtoolsKitModule
|
|
132
|
+
);
|
|
133
|
+
addCustomTab({
|
|
134
|
+
name: "nuxt-api-contract",
|
|
135
|
+
title: "API Contracts",
|
|
136
|
+
icon: "carbon:api",
|
|
137
|
+
view: { type: "iframe", src: devtoolsRoute }
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ZodType } from 'zod';
|
|
2
|
+
import { AnyApiContract } from './client.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OpenAPI generation layer. Completely isolated from runtime validation:
|
|
6
|
+
* consumes contract objects, produces a JSON-serializable document.
|
|
7
|
+
*
|
|
8
|
+
* Unsupported Zod features (transform / refine / superRefine / preprocess)
|
|
9
|
+
* are represented as closely as possible, a warning is collected and
|
|
10
|
+
* generation of the whole document never fails.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type JsonSchemaObject = Record<string, unknown>;
|
|
14
|
+
interface GenerationWarning {
|
|
15
|
+
contract: string;
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
interface OpenApiOptions {
|
|
19
|
+
title?: string;
|
|
20
|
+
version?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
}
|
|
23
|
+
interface OpenApiGenerationResult {
|
|
24
|
+
document: Record<string, unknown>;
|
|
25
|
+
warnings: GenerationWarning[];
|
|
26
|
+
}
|
|
27
|
+
/** Converts a path like `/api/users/:id` into `/api/users/{id}`. */
|
|
28
|
+
declare function toOpenApiPath(path: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Converts a Zod schema into a JSON schema. Best-effort: anything not
|
|
31
|
+
* directly representable falls back to the inner schema or an empty schema
|
|
32
|
+
* plus a warning.
|
|
33
|
+
*/
|
|
34
|
+
declare function zodToJsonSchema(schema: ZodType, warnings: GenerationWarning[], contractLabel: string): JsonSchemaObject;
|
|
35
|
+
/** Builds one OpenAPI operation from a contract. */
|
|
36
|
+
declare function contractToOperation(contract: AnyApiContract, warnings: GenerationWarning[]): JsonSchemaObject;
|
|
37
|
+
/**
|
|
38
|
+
* Generates a complete OpenAPI 3.0 document from contracts.
|
|
39
|
+
* Never throws: invalid pieces degrade to warnings.
|
|
40
|
+
*/
|
|
41
|
+
declare function generateOpenApiDocument(contracts: AnyApiContract[], options?: OpenApiOptions): OpenApiGenerationResult;
|
|
42
|
+
/** Helper for CLI / tooling: filters a list of unknown values down to contracts. */
|
|
43
|
+
declare function pickContracts(values: unknown[]): AnyApiContract[];
|
|
44
|
+
|
|
45
|
+
export { contractToOperation, generateOpenApiDocument, pickContracts, toOpenApiPath, zodToJsonSchema };
|
|
46
|
+
export type { GenerationWarning, JsonSchemaObject, OpenApiGenerationResult, OpenApiOptions };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ZodType } from 'zod';
|
|
2
|
+
import { AnyApiContract } from './client.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OpenAPI generation layer. Completely isolated from runtime validation:
|
|
6
|
+
* consumes contract objects, produces a JSON-serializable document.
|
|
7
|
+
*
|
|
8
|
+
* Unsupported Zod features (transform / refine / superRefine / preprocess)
|
|
9
|
+
* are represented as closely as possible, a warning is collected and
|
|
10
|
+
* generation of the whole document never fails.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type JsonSchemaObject = Record<string, unknown>;
|
|
14
|
+
interface GenerationWarning {
|
|
15
|
+
contract: string;
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
interface OpenApiOptions {
|
|
19
|
+
title?: string;
|
|
20
|
+
version?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
}
|
|
23
|
+
interface OpenApiGenerationResult {
|
|
24
|
+
document: Record<string, unknown>;
|
|
25
|
+
warnings: GenerationWarning[];
|
|
26
|
+
}
|
|
27
|
+
/** Converts a path like `/api/users/:id` into `/api/users/{id}`. */
|
|
28
|
+
declare function toOpenApiPath(path: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Converts a Zod schema into a JSON schema. Best-effort: anything not
|
|
31
|
+
* directly representable falls back to the inner schema or an empty schema
|
|
32
|
+
* plus a warning.
|
|
33
|
+
*/
|
|
34
|
+
declare function zodToJsonSchema(schema: ZodType, warnings: GenerationWarning[], contractLabel: string): JsonSchemaObject;
|
|
35
|
+
/** Builds one OpenAPI operation from a contract. */
|
|
36
|
+
declare function contractToOperation(contract: AnyApiContract, warnings: GenerationWarning[]): JsonSchemaObject;
|
|
37
|
+
/**
|
|
38
|
+
* Generates a complete OpenAPI 3.0 document from contracts.
|
|
39
|
+
* Never throws: invalid pieces degrade to warnings.
|
|
40
|
+
*/
|
|
41
|
+
declare function generateOpenApiDocument(contracts: AnyApiContract[], options?: OpenApiOptions): OpenApiGenerationResult;
|
|
42
|
+
/** Helper for CLI / tooling: filters a list of unknown values down to contracts. */
|
|
43
|
+
declare function pickContracts(values: unknown[]): AnyApiContract[];
|
|
44
|
+
|
|
45
|
+
export { contractToOperation, generateOpenApiDocument, pickContracts, toOpenApiPath, zodToJsonSchema };
|
|
46
|
+
export type { GenerationWarning, JsonSchemaObject, OpenApiGenerationResult, OpenApiOptions };
|