kubb-plugin-sveltekit-remote-functions 1.0.0-20260913.9
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 +160 -0
- package/dist/generators/remoteFunctionGenerator.d.ts +9 -0
- package/dist/generators/remoteFunctionGenerator.js +173 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/kindResolution.d.ts +29 -0
- package/dist/kindResolution.js +50 -0
- package/dist/naming.d.ts +11 -0
- package/dist/naming.js +44 -0
- package/dist/plugin.d.ts +48 -0
- package/dist/plugin.js +104 -0
- package/dist/resolveOperations.d.ts +84 -0
- package/dist/resolveOperations.js +113 -0
- package/dist/resolver.d.ts +7 -0
- package/dist/resolver.js +20 -0
- package/dist/templates.d.ts +6 -0
- package/dist/templates.js +7 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.js +19 -0
- package/package.json +78 -0
- package/src/generators/remoteFunctionGenerator.ts +226 -0
- package/src/index.ts +3 -0
- package/src/kindResolution.ts +74 -0
- package/src/naming.ts +49 -0
- package/src/plugin.ts +123 -0
- package/src/resolveOperations.ts +188 -0
- package/src/resolver.ts +21 -0
- package/src/templates.ts +8 -0
- package/src/types.ts +104 -0
- package/templates/.prettierrc +3 -0
- package/templates/sveltekitTransport.ts +137 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 iturner100
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href] [![npm downloads][npm-downloads-src]][npm-downloads-href] [![License][license-src]][license-href]
|
|
4
|
+
|
|
5
|
+
<h4>
|
|
6
|
+
<a href="https://kubb.dev" target="_blank" rel="noopener noreferrer">Kubb</a>
|
|
7
|
+
<span> · </span>
|
|
8
|
+
<a href="https://github.com/iturner100/kubb-plugin-sveltekit-remote-functions/issues" target="_blank" rel="noopener noreferrer">Report Bug</a>
|
|
9
|
+
<span> · </span>
|
|
10
|
+
<a href="https://github.com/iturner100/kubb-plugin-sveltekit-remote-functions/issues" target="_blank" rel="noopener noreferrer">Request Feature</a>
|
|
11
|
+
</h4>
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<br />
|
|
16
|
+
|
|
17
|
+
# kubb-plugin-sveltekit-remote-functions
|
|
18
|
+
|
|
19
|
+
### Generate SvelteKit remote functions with Kubb
|
|
20
|
+
|
|
21
|
+
Generates [SvelteKit remote functions](https://svelte.dev/docs/kit/remote-functions) (`query`, `query.batch`, `query.live`, `command`, `form`, `prerender`) from an OpenAPI spec — one `*.remote.ts` file per operation. This plugin does not perform HTTP calls itself: it depends on [`@kubb/plugin-fetch`](https://kubb.dev/plugins/plugin-fetch) and wraps the async function that plugin already generates for each operation, swapping in SvelteKit's request-scoped `event.fetch` via a [custom transport](https://kubb.dev/plugins/plugin-fetch/guide/transport) and registering a pass-through [response interceptor](https://kubb.dev/plugins/plugin-fetch/guide/ interceptors) as an extension point for future SvelteKit-specific response handling.
|
|
22
|
+
|
|
23
|
+
Every generated remote function uses the operation's [`@kubb/plugin-zod`](https://kubb.dev/plugins/plugin-zod) combined "options" schema directly as its argument schema, so this plugin has two hard configuration requirements (see [Requirements](#requirements)).
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
pnpm add kubb-plugin-sveltekit-remote-functions @kubb/plugin-fetch @kubb/plugin-zod
|
|
29
|
+
# or
|
|
30
|
+
npm install kubb-plugin-sveltekit-remote-functions @kubb/plugin-fetch @kubb/plugin-zod
|
|
31
|
+
# or
|
|
32
|
+
bun add kubb-plugin-sveltekit-remote-functions @kubb/plugin-fetch @kubb/plugin-zod
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- `pluginFetch()` and `pluginZod()` must both be present in the same pipeline — hard, driver-enforced dependencies. This plugin throws a descriptive error at startup if either is missing.
|
|
38
|
+
- `pluginZod({ inferred: true })` — `@kubb/plugin-zod` only generates the combined `{ body, path, query, headers }` "options" schema this plugin uses directly (as well as the per-parameter grouped schemas it's built from) when `inferred: true`.
|
|
39
|
+
- `pluginFetch({ validator: { request: "zod", response: "zod" } })` — both request and response must be Zod-validated. The bare `validator: "zod"` shorthand only validates the response, which is not sufficient.
|
|
40
|
+
|
|
41
|
+
When either requirement isn't met, this plugin reports a warning and skips generating any `*.remote.ts` files, without failing the rest of the Kubb run.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { defineConfig } from "kubb/config";
|
|
47
|
+
import { pluginZod } from "@kubb/plugin-zod";
|
|
48
|
+
import { pluginFetch } from "@kubb/plugin-fetch";
|
|
49
|
+
import { pluginSveltekitRemoteFunctions } from "kubb-plugin-sveltekit-remote-functions";
|
|
50
|
+
|
|
51
|
+
export default defineConfig({
|
|
52
|
+
input: { path: "./petStore.yaml" },
|
|
53
|
+
output: { path: "./src/lib/generated", clean: true },
|
|
54
|
+
plugins: [
|
|
55
|
+
pluginZod({ inferred: true }),
|
|
56
|
+
pluginFetch({ output: { path: "./clients" }, validator: { request: "zod", response: "zod" } }),
|
|
57
|
+
pluginSveltekitRemoteFunctions(),
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
SvelteKit only allows remote-function files under `src` (not `src/lib/server`), so point this plugin's `output.path` accordingly — note the example above resolves it as a sibling of the config's own `output.path`, not `pluginFetch()`'s.
|
|
63
|
+
|
|
64
|
+
## How the remote-function kind is chosen
|
|
65
|
+
|
|
66
|
+
By default, the HTTP method decides the kind(s) generated for an operation:
|
|
67
|
+
|
|
68
|
+
| Method | Generated kind(s) |
|
|
69
|
+
| -------------------------------- | ---------------------------------------------------------------- |
|
|
70
|
+
| `GET` | `query` |
|
|
71
|
+
| `POST`, `PUT`, `PATCH`, `DELETE` | `command` (plus `form` when `sveltekit.generateForms` is `true`) |
|
|
72
|
+
|
|
73
|
+
Override this per operation with the `x-sveltekit-remote-function-type` OpenAPI vendor extension, an array of any of: `"query"`, `"query.batch"`, `"query.live"`, `"command"`, `"form"`, `"prerender"`. An explicit extension always takes precedence over `sveltekit.generateForms`.
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
paths:
|
|
77
|
+
/pets/{petId}:
|
|
78
|
+
delete:
|
|
79
|
+
operationId: deletePet
|
|
80
|
+
x-sveltekit-remote-function-type: ["command"]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
An invalid extension value (not an array, empty, or containing an unrecognized string) throws a descriptive error naming the operation and the offending value.
|
|
84
|
+
|
|
85
|
+
## Naming when an operation generates more than one kind
|
|
86
|
+
|
|
87
|
+
`query` and `command` are normally unsuffixed. Additional kinds get a suffix appended to the base operation name (the same camelCase name `@kubb/plugin-fetch` uses for the wrapped function):
|
|
88
|
+
|
|
89
|
+
| Kind | Suffix |
|
|
90
|
+
| ------------- | ------------------------------------------------------------------------------- |
|
|
91
|
+
| `query` | _(none, unless `command` is also generated for the same operation — see below)_ |
|
|
92
|
+
| `command` | _(none, unless `query` is also generated for the same operation — see below)_ |
|
|
93
|
+
| `form` | `Form` |
|
|
94
|
+
| `query.batch` | `Batch` |
|
|
95
|
+
| `query.live` | `Live` |
|
|
96
|
+
| `prerender` | _(none if the only kind, else `Prerender`)_ |
|
|
97
|
+
|
|
98
|
+
For example, `POST /pets` with `sveltekit.generateForms: true` (`command` + `form`) generates `createPet` (a `command`) and `createPetForm` (a `form`). `query` and `command` are mutually exclusive under the default method-based resolution, so there's normally no ambiguity in leaving either bare; if an explicit `x-sveltekit-remote-function-type` override lists both together for the same operation, they fall back to their own suffix (`Query`/`Command`) instead, to avoid a duplicate export.
|
|
99
|
+
|
|
100
|
+
The wrapped `@kubb/plugin-fetch` function is always imported under a `Fetch`-suffixed alias (e.g. `import { createPet as createPetFetch } from ...`), decoupled from the exported remote-function name(s), so an unsuffixed `query`/`command` export can never collide with the import of the function it wraps.
|
|
101
|
+
|
|
102
|
+
## Explicit request/response typing
|
|
103
|
+
|
|
104
|
+
Every generated remote function's handler is explicitly annotated with the types `@kubb/plugin-zod` infers for that operation, instead of relying only on inference from the schema value passed into `query`/`command`/`form`/`prerender`/`query.batch`/`query.live`:
|
|
105
|
+
|
|
106
|
+
- The handler's input parameter(s) are typed as `<Name>OptionsSchemaType` — the combined `{ body, path, query, headers }` options type, resolved via `@kubb/plugin-zod`'s public `resolver.response.options(node)` accessor.
|
|
107
|
+
- The handler's return value(s) are typed as `<Name>ResponseSchemaType` — the union of the operation's success responses, resolved via `resolver.response.response(node)` (the schema's value name) combined with `resolver.schema.type(name)`.
|
|
108
|
+
|
|
109
|
+
For example:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
export const createPet = command(createPetOptionsSchema, async (input: CreatePetOptionsSchemaType): Promise<CreatePetResponseSchemaType> => {
|
|
113
|
+
const { fetch } = getRequestEvent();
|
|
114
|
+
const transport = createSvelteKitTransport(fetch);
|
|
115
|
+
return createPetFetch({ ...input, transport }).unwrap();
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`query.batch` and `query.live` annotate every intermediate value too (the `inputs` array, the `results` array, and the returned per-item mapper's own parameter/return types for `query.batch`; the generator function's parameter and its `AsyncGenerator<...>` return type for `query.live`).
|
|
120
|
+
|
|
121
|
+
This doesn't change runtime behavior — SvelteKit already infers these same types from the schema value — but it turns any drift between what `@kubb/plugin-zod` infers and what the wrapped `@kubb/plugin-fetch` function actually expects/returns into an immediate, readable compile error in the generated file, and makes each `.remote.ts` file self-documenting without having to trace through the schema value to see its shape.
|
|
122
|
+
|
|
123
|
+
## Options
|
|
124
|
+
|
|
125
|
+
This plugin's `Options` type extends `@kubb/plugin-fetch`'s own `Options` minus `validator` (`output`, `include`, `exclude`, `override`, `baseURL`, `group`, `sdk`, `resolver`), so configuring it feels the same as configuring `pluginFetch()`, plus one plugin-specific field:
|
|
126
|
+
|
|
127
|
+
- `sveltekit.liveQueryPollIntervalMs?: number` — poll interval (ms) used by the generated `query.live` async generator body. Default `5000`.
|
|
128
|
+
- `sveltekit.generateForms?: boolean` — controls the **default**, method-based kind resolution: when `false` (the default), `POST | PUT | PATCH | DELETE` operations generate just `command`; when `true`, they generate both `command` and `form`. Has no effect on operations that declare the `x-sveltekit-remote-function-type` extension — the extension always wins.
|
|
129
|
+
|
|
130
|
+
Notes on the reused fields, since this plugin wraps `@kubb/plugin-fetch`'s functions rather than performing HTTP calls itself:
|
|
131
|
+
|
|
132
|
+
- `output` — where this plugin's own `*.remote.ts` files are written; independent of the `pluginFetch()` instance's own `output`.
|
|
133
|
+
- `group` — defaults to `{ type: 'tag' }` (unlike `@kubb/plugin-fetch`, which defaults to no grouping), so that two operations that share an `operationId` but live under different tags resolve to different output paths instead of silently colliding into a single file with duplicate exports. Override to `null`/`undefined` for a flat `output.path` instead, but only if you're confident every operation's `operationId` is unique across the whole spec.
|
|
134
|
+
- `validator` — omitted entirely, unlike `@kubb/plugin-fetch`'s `Options`. Every remote function always validates its argument against the operation's `@kubb/plugin-zod` "options" schema; there is no "unchecked" mode. See [Requirements](#requirements).
|
|
135
|
+
- `baseURL`, `sdk` — accepted for shape parity but are no-ops: the wrapped call already carries its own `baseURL`, and remote functions are always standalone exports, never a class-based SDK.
|
|
136
|
+
|
|
137
|
+
## Extension points
|
|
138
|
+
|
|
139
|
+
- **Custom transport** (`templates/sveltekitTransport.ts`, copied into `.kubb/sveltekitTransport.ts` next to `@kubb/plugin-fetch`'s own `.kubb/client.ts`): `createSvelteKitTransport(fetch)` builds a `Transport` that sends requests through the given `fetch` implementation — every generated remote function calls it with `getRequestEvent().fetch` so requests reuse the incoming request's credentials/origin. Copy and modify this file if you need different request-building behavior.
|
|
140
|
+
|
|
141
|
+
## Known limitations
|
|
142
|
+
|
|
143
|
+
- `form`'s schema is the same combined `{ body, path, query, headers }` options schema used by every other kind (not just the raw request body), since SvelteKit's `form` handler receives the parsed argument object directly and that object is exactly what the wrapped fetch call expects. Submitted form fields must therefore be named/nested to match that shape (e.g. `body.name`, `path.petId`), not just the body's own fields.
|
|
144
|
+
- SvelteKit's `form()` is stricter than `command()`/`query()` about the schema's inferred shape (e.g. every boolean must be optional). Operations whose request body is modeled as a Zod `union` of several content-type variants (common for specs that declare the same body under multiple content types) can fail this stricter `form()` type-check even though `command()` for the same operation compiles fine — a pre-existing consequence of how `@kubb/plugin-zod` models those bodies, not something this plugin's schema selection introduces.
|
|
145
|
+
- `query.batch` does not perform true HTTP-level batching — it satisfies SvelteKit's `(input, index) => output` batching contract by calling the wrapped operation once per input.
|
|
146
|
+
- `query.live` generates a basic polling implementation (`sveltekit.liveQueryPollIntervalMs`), not push-based live data; customize the generated file for real-time use cases.
|
|
147
|
+
- `prerender`'s per-operation `inputs`/`dynamic` options are not currently generated.
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
[MIT](../LICENSE)
|
|
152
|
+
|
|
153
|
+
<!-- Badges -->
|
|
154
|
+
|
|
155
|
+
[npm-version-src]: https://img.shields.io/npm/v/kubb-plugin-sveltekit-remote-functions.svg
|
|
156
|
+
[npm-version-href]: https://www.npmjs.com/package/kubb-plugin-sveltekit-remote-functions
|
|
157
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/kubb-plugin-sveltekit-remote-functions.svg
|
|
158
|
+
[npm-downloads-href]: https://www.npmjs.com/package/kubb-plugin-sveltekit-remote-functions
|
|
159
|
+
[license-src]: https://img.shields.io/npm/l/kubb-plugin-sveltekit-remote-functions.svg
|
|
160
|
+
[license-href]: ../LICENSE
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Generator } from "kubb/kit";
|
|
2
|
+
import type { PluginSveltekitRemoteFunctions } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Core generator for `kubb-plugin-sveltekit-remote-functions`. For each HTTP operation, resolves
|
|
5
|
+
* the SvelteKit remote-function kind(s) it should generate (method-based, or overridden by
|
|
6
|
+
* `x-sveltekit-remote-function-type`), locates the `@kubb/plugin-fetch` function it wraps, and
|
|
7
|
+
* emits one `<operationName>.remote.ts` file per operation containing every resolved kind.
|
|
8
|
+
*/
|
|
9
|
+
export declare const remoteFunctionGenerator: Generator<PluginSveltekitRemoteFunctions>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ast } from "kubb/kit";
|
|
3
|
+
import { resolveRemoteFunctionKinds } from "../kindResolution.js";
|
|
4
|
+
import { remoteFunctionName } from "../naming.js";
|
|
5
|
+
import { checkPreconditions, resolveFetchOperation, resolveZodOperation } from "../resolveOperations.js";
|
|
6
|
+
/**
|
|
7
|
+
* Converts an absolute file path into a relative ESM import specifier (POSIX separators, no
|
|
8
|
+
* extension, `./`-prefixed for sibling files), computed from the importing file's own directory.
|
|
9
|
+
*/
|
|
10
|
+
function toImportSpecifier(fromFilePath, toFilePath) {
|
|
11
|
+
const relative = path.relative(path.dirname(fromFilePath), toFilePath);
|
|
12
|
+
const withoutExt = relative.replace(/\.[cm]?tsx?$/, "");
|
|
13
|
+
const posix = withoutExt.split(path.sep).join("/");
|
|
14
|
+
return posix.startsWith(".") ? posix : `./${posix}`;
|
|
15
|
+
}
|
|
16
|
+
const svelteImportByKind = {
|
|
17
|
+
query: "query",
|
|
18
|
+
"query.batch": "query",
|
|
19
|
+
"query.live": "query",
|
|
20
|
+
command: "command",
|
|
21
|
+
form: "form",
|
|
22
|
+
prerender: "prerender",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Builds the *.remote.ts source for one remote-function kind of an operation.
|
|
26
|
+
*/
|
|
27
|
+
function buildRemoteFunctionSource(options) {
|
|
28
|
+
const { kind, exportName, fetchName, argumentSchemaExpression, optionsSchemaTypeName, responseSchemaTypeName, pollIntervalMs, resultAccessor } = options;
|
|
29
|
+
switch (kind) {
|
|
30
|
+
case "query":
|
|
31
|
+
case "command":
|
|
32
|
+
case "form":
|
|
33
|
+
case "prerender":
|
|
34
|
+
return `export const ${exportName} = ${kind}(${argumentSchemaExpression}, async (input: ${optionsSchemaTypeName}): Promise<${responseSchemaTypeName}> => {
|
|
35
|
+
const { fetch } = getRequestEvent()
|
|
36
|
+
const transport = createSvelteKitTransport(fetch)
|
|
37
|
+
return ${fetchName}({ ...input, transport })${resultAccessor}
|
|
38
|
+
})`;
|
|
39
|
+
case "query.batch":
|
|
40
|
+
// No true HTTP batching: each input still triggers its own call to the wrapped
|
|
41
|
+
// \`@kubb/plugin-fetch\` function, deduped only at the SvelteKit layer.
|
|
42
|
+
return `export const ${exportName} = query.batch(${argumentSchemaExpression}, async (inputs: ${optionsSchemaTypeName}[]) => {
|
|
43
|
+
const { fetch } = getRequestEvent()
|
|
44
|
+
const transport = createSvelteKitTransport(fetch)
|
|
45
|
+
const results: ${responseSchemaTypeName}[] = await Promise.all(
|
|
46
|
+
inputs.map((input: ${optionsSchemaTypeName}) => ${fetchName}({ ...input, transport })${resultAccessor}),
|
|
47
|
+
)
|
|
48
|
+
return (_input: ${optionsSchemaTypeName}, index: number): ${responseSchemaTypeName} => results[index]
|
|
49
|
+
})`;
|
|
50
|
+
case "query.live":
|
|
51
|
+
// Basic polling default; true push-based live data needs bespoke server logic.
|
|
52
|
+
return `export const ${exportName} = query.live(${argumentSchemaExpression}, async function* (input: ${optionsSchemaTypeName}): AsyncGenerator<${responseSchemaTypeName}> {
|
|
53
|
+
const { fetch } = getRequestEvent()
|
|
54
|
+
const transport = createSvelteKitTransport(fetch)
|
|
55
|
+
while (true) {
|
|
56
|
+
yield await ${fetchName}({ ...input, transport })${resultAccessor}
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, ${pollIntervalMs}))
|
|
58
|
+
}
|
|
59
|
+
})`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Core generator for `kubb-plugin-sveltekit-remote-functions`. For each HTTP operation, resolves
|
|
64
|
+
* the SvelteKit remote-function kind(s) it should generate (method-based, or overridden by
|
|
65
|
+
* `x-sveltekit-remote-function-type`), locates the `@kubb/plugin-fetch` function it wraps, and
|
|
66
|
+
* emits one `<operationName>.remote.ts` file per operation containing every resolved kind.
|
|
67
|
+
*/
|
|
68
|
+
export const remoteFunctionGenerator = {
|
|
69
|
+
name: "sveltekit-remote-functions",
|
|
70
|
+
match(node) {
|
|
71
|
+
return node.kind === "Operation" && ast.isHttpOperationNode(node);
|
|
72
|
+
},
|
|
73
|
+
operation(node, ctx) {
|
|
74
|
+
if (!ast.isHttpOperationNode(node)) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const kinds = resolveRemoteFunctionKinds(node, ctx.adapter.document, ctx.plugin.options?.sveltekit?.generateForms ?? false);
|
|
78
|
+
if (kinds.length === 0) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
// Hard requirements: `@kubb/plugin-zod` must be configured with `inferred: true` and
|
|
82
|
+
// `@kubb/plugin-fetch` with `validator: { request: 'zod', response: 'zod' }`. Skip generating
|
|
83
|
+
// anything (with a one-time warning) rather than failing the build when either is missing.
|
|
84
|
+
if (!checkPreconditions(ctx)) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const fetchOp = resolveFetchOperation(node, ctx);
|
|
88
|
+
const zodOp = resolveZodOperation(node, ctx);
|
|
89
|
+
const root = path.resolve(ctx.config.root, ctx.config.output.path);
|
|
90
|
+
const file = ctx.resolver.file({
|
|
91
|
+
name: node.operationId,
|
|
92
|
+
extname: ".remote.ts",
|
|
93
|
+
tag: node.tags[0] ?? "default",
|
|
94
|
+
path: node.path,
|
|
95
|
+
root,
|
|
96
|
+
output: ctx.options.output,
|
|
97
|
+
group: ctx.options.group ?? undefined,
|
|
98
|
+
});
|
|
99
|
+
const baseName = ctx.resolver.name(node.operationId);
|
|
100
|
+
// Import the wrapped `@kubb/plugin-fetch` function under a fixed `Fetch`-suffixed alias,
|
|
101
|
+
// decoupled from whatever the remote-function export name(s) turn out to be. `Fetch` is never
|
|
102
|
+
// one of this plugin's own kind suffixes (`Query`/`Command`/`Form`/`Batch`/`Live`/`Prerender`),
|
|
103
|
+
// so the alias can never collide with an exported remote function in the same file — e.g. a
|
|
104
|
+
// `command` kind that resolves to the unsuffixed base name would otherwise clash with an
|
|
105
|
+
// import of the same name.
|
|
106
|
+
const fetchImportAlias = `${fetchOp.name}Fetch`;
|
|
107
|
+
const svelteImports = new Set(["getRequestEvent"]);
|
|
108
|
+
for (const kind of kinds) {
|
|
109
|
+
svelteImports.add(svelteImportByKind[kind]);
|
|
110
|
+
}
|
|
111
|
+
// The combined `{ body, path, query, headers }` options schema `@kubb/plugin-zod` generates
|
|
112
|
+
// (only when `inferred: true`, guaranteed by `checkPreconditions`) is used directly, unmodified,
|
|
113
|
+
// as the argument schema for every kind, `form` included: it's exactly the shape the wrapped
|
|
114
|
+
// `@kubb/plugin-fetch` function's own `options` argument expects.
|
|
115
|
+
const optionsSchemaExpression = zodOp.optionsSchemaName;
|
|
116
|
+
const sources = [];
|
|
117
|
+
// `.unwrap()` only exists when the wrapped `pluginFetch()` instance uses its default
|
|
118
|
+
// `returnType: 'full'`. With `returnType: 'data'` the wrapped call already resolves to the
|
|
119
|
+
// bare success body directly (see `FetchOperation.returnType`'s doc comment), so calling
|
|
120
|
+
// `.unwrap()` on it would be a compile error.
|
|
121
|
+
const resultAccessor = fetchOp.returnType === "data" ? "" : ".unwrap()";
|
|
122
|
+
for (const kind of kinds) {
|
|
123
|
+
const exportName = remoteFunctionName(baseName, kind, kinds);
|
|
124
|
+
sources.push(buildRemoteFunctionSource({
|
|
125
|
+
kind,
|
|
126
|
+
exportName,
|
|
127
|
+
fetchName: fetchImportAlias,
|
|
128
|
+
argumentSchemaExpression: optionsSchemaExpression,
|
|
129
|
+
optionsSchemaTypeName: zodOp.optionsSchemaTypeName,
|
|
130
|
+
responseSchemaTypeName: zodOp.responseSchemaTypeName,
|
|
131
|
+
pollIntervalMs: ctx.options.sveltekit.liveQueryPollIntervalMs,
|
|
132
|
+
resultAccessor,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
const kubbDir = path.dirname(fetchOp.clientPath);
|
|
136
|
+
const sveltekitTransportPath = path.join(kubbDir, "sveltekitTransport.ts");
|
|
137
|
+
const imports = [
|
|
138
|
+
ast.factory.createImport({
|
|
139
|
+
name: Array.from(svelteImports),
|
|
140
|
+
path: "$app/server",
|
|
141
|
+
}),
|
|
142
|
+
ast.factory.createImport({
|
|
143
|
+
name: [{ propertyName: fetchOp.name, name: fetchImportAlias }],
|
|
144
|
+
path: toImportSpecifier(file.path, fetchOp.path),
|
|
145
|
+
}),
|
|
146
|
+
ast.factory.createImport({
|
|
147
|
+
name: ["createSvelteKitTransport"],
|
|
148
|
+
path: toImportSpecifier(file.path, sveltekitTransportPath),
|
|
149
|
+
}),
|
|
150
|
+
ast.factory.createImport({
|
|
151
|
+
name: [zodOp.optionsSchemaName],
|
|
152
|
+
path: toImportSpecifier(file.path, zodOp.path),
|
|
153
|
+
}),
|
|
154
|
+
ast.factory.createImport({
|
|
155
|
+
name: [zodOp.optionsSchemaTypeName, zodOp.responseSchemaTypeName],
|
|
156
|
+
path: toImportSpecifier(file.path, zodOp.path),
|
|
157
|
+
isTypeOnly: true,
|
|
158
|
+
}),
|
|
159
|
+
];
|
|
160
|
+
const fileNode = ast.factory.createFile({
|
|
161
|
+
baseName: file.baseName,
|
|
162
|
+
path: file.path,
|
|
163
|
+
imports,
|
|
164
|
+
sources: [
|
|
165
|
+
ast.factory.createSource({
|
|
166
|
+
nodes: sources.map((source) => ast.factory.createText(source)),
|
|
167
|
+
}),
|
|
168
|
+
],
|
|
169
|
+
exports: [],
|
|
170
|
+
});
|
|
171
|
+
return [fileNode];
|
|
172
|
+
},
|
|
173
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { pluginSveltekitRemoteFunctions, default } from "./plugin.js";
|
|
2
|
+
export { pluginSveltekitRemoteFunctionsName, remoteFunctionTypeExtensionKey } from "./types.js";
|
|
3
|
+
export type { Options, PluginSveltekitRemoteFunctions, RemoteFunctionKind, ResolvedOptions, SveltekitOptions } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ast } from "kubb/kit";
|
|
2
|
+
import { type RemoteFunctionKind } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Minimal, defensive shape of the parsed OpenAPI document needed to read vendor extensions off an
|
|
5
|
+
* operation. `GeneratorContext['adapter']['document']` is typed `unknown` at the `@kubb/core`
|
|
6
|
+
* level (the concrete `Document` type lives in the adapter package, e.g. `@kubb/adapter-oas`), so
|
|
7
|
+
* this plugin reads through a loose, structurally-typed view instead of depending on the adapter
|
|
8
|
+
* package directly.
|
|
9
|
+
*/
|
|
10
|
+
export type LooseOpenApiDocument = {
|
|
11
|
+
paths?: Record<string, Record<string, Record<string, unknown>> | undefined>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Reads the raw operation object (the `get`/`post`/... entry on a path item, including vendor
|
|
15
|
+
* extensions) for an `HttpOperationNode` out of the adapter's parsed document.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getRawOperationObject(node: ast.HttpOperationNode, document: unknown): Record<string, unknown> | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Resolves which SvelteKit remote-function kinds an operation should generate.
|
|
20
|
+
*
|
|
21
|
+
* - If the operation carries `x-sveltekit-remote-function-type`, that array is used as-is (after
|
|
22
|
+
* validating every entry and de-duplicating) — regardless of `generateForms`.
|
|
23
|
+
* - Otherwise, the HTTP method decides: `GET` → `['query']`; `POST | PUT | PATCH | DELETE` →
|
|
24
|
+
* `['command']`, plus `'form'` when `generateForms` is `true`.
|
|
25
|
+
*
|
|
26
|
+
* Throws a descriptive error when the extension is present but malformed (not an array, empty, or
|
|
27
|
+
* containing an unknown value), since that is almost certainly a spec authoring mistake.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveRemoteFunctionKinds(node: ast.HttpOperationNode, document: unknown, generateForms: boolean): Array<RemoteFunctionKind>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { remoteFunctionTypeExtensionKey } from "./types.js";
|
|
2
|
+
const allKinds = new Set(["query", "query.batch", "query.live", "command", "form", "prerender"]);
|
|
3
|
+
function isRemoteFunctionKind(value) {
|
|
4
|
+
return typeof value === "string" && allKinds.has(value);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Reads the raw operation object (the `get`/`post`/... entry on a path item, including vendor
|
|
8
|
+
* extensions) for an `HttpOperationNode` out of the adapter's parsed document.
|
|
9
|
+
*/
|
|
10
|
+
export function getRawOperationObject(node, document) {
|
|
11
|
+
const doc = document;
|
|
12
|
+
const method = node.method.toLowerCase();
|
|
13
|
+
return doc?.paths?.[node.path]?.[method];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolves which SvelteKit remote-function kinds an operation should generate.
|
|
17
|
+
*
|
|
18
|
+
* - If the operation carries `x-sveltekit-remote-function-type`, that array is used as-is (after
|
|
19
|
+
* validating every entry and de-duplicating) — regardless of `generateForms`.
|
|
20
|
+
* - Otherwise, the HTTP method decides: `GET` → `['query']`; `POST | PUT | PATCH | DELETE` →
|
|
21
|
+
* `['command']`, plus `'form'` when `generateForms` is `true`.
|
|
22
|
+
*
|
|
23
|
+
* Throws a descriptive error when the extension is present but malformed (not an array, empty, or
|
|
24
|
+
* containing an unknown value), since that is almost certainly a spec authoring mistake.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveRemoteFunctionKinds(node, document, generateForms) {
|
|
27
|
+
const raw = getRawOperationObject(node, document);
|
|
28
|
+
const extensionValue = raw?.[remoteFunctionTypeExtensionKey];
|
|
29
|
+
if (extensionValue !== undefined) {
|
|
30
|
+
if (!Array.isArray(extensionValue) || extensionValue.length === 0) {
|
|
31
|
+
throw new Error(`Operation "${node.operationId}" declares "${remoteFunctionTypeExtensionKey}" but its value is not a non-empty array (got: ${JSON.stringify(extensionValue)}).`);
|
|
32
|
+
}
|
|
33
|
+
const invalid = extensionValue.filter((value) => !isRemoteFunctionKind(value));
|
|
34
|
+
if (invalid.length > 0) {
|
|
35
|
+
throw new Error(`Operation "${node.operationId}" declares "${remoteFunctionTypeExtensionKey}" with unsupported value(s): ${JSON.stringify(invalid)}. Valid values are: ${Array.from(allKinds).join(", ")}.`);
|
|
36
|
+
}
|
|
37
|
+
return Array.from(new Set(extensionValue));
|
|
38
|
+
}
|
|
39
|
+
switch (node.method.toUpperCase()) {
|
|
40
|
+
case "GET":
|
|
41
|
+
return ["query"];
|
|
42
|
+
case "POST":
|
|
43
|
+
case "PUT":
|
|
44
|
+
case "PATCH":
|
|
45
|
+
case "DELETE":
|
|
46
|
+
return generateForms ? ["command", "form"] : ["command"];
|
|
47
|
+
default:
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/naming.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RemoteFunctionKind } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the exported identifier for one remote-function kind of an operation.
|
|
4
|
+
*
|
|
5
|
+
* @param baseName - The operation's base name, e.g. `resolver.name(operationId)` (camelCase,
|
|
6
|
+
* matching the name `@kubb/plugin-fetch` itself would use for the wrapped function).
|
|
7
|
+
* @param kind - The remote-function kind being named.
|
|
8
|
+
* @param allKinds - Every kind generated for this operation, used to decide whether `query`,
|
|
9
|
+
* `command`, or `prerender` need a suffix.
|
|
10
|
+
*/
|
|
11
|
+
export declare function remoteFunctionName(baseName: string, kind: RemoteFunctionKind, allKinds: Array<RemoteFunctionKind>): string;
|
package/dist/naming.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suffix applied to the base operation name for a given remote-function kind when it isn't left
|
|
3
|
+
* bare (e.g. `POST /pets` → `command` `createPet` + `form` `createPetForm`).
|
|
4
|
+
*
|
|
5
|
+
* `query` and `command` are normally unsuffixed ("primary"): an operation's default resolution
|
|
6
|
+
* never produces both for the same operation, so there is no ambiguity in leaving either bare.
|
|
7
|
+
* They only fall back to their own suffix (`Query`/`Command`) in the rare case where an explicit
|
|
8
|
+
* `x-sveltekit-remote-function-type` override lists both together (see `remoteFunctionName`).
|
|
9
|
+
* `prerender` is unsuffixed only when it is the sole kind generated for an operation.
|
|
10
|
+
*/
|
|
11
|
+
const suffixByKind = {
|
|
12
|
+
query: "Query",
|
|
13
|
+
command: "Command",
|
|
14
|
+
form: "Form",
|
|
15
|
+
"query.batch": "Batch",
|
|
16
|
+
"query.live": "Live",
|
|
17
|
+
prerender: "Prerender",
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the exported identifier for one remote-function kind of an operation.
|
|
21
|
+
*
|
|
22
|
+
* @param baseName - The operation's base name, e.g. `resolver.name(operationId)` (camelCase,
|
|
23
|
+
* matching the name `@kubb/plugin-fetch` itself would use for the wrapped function).
|
|
24
|
+
* @param kind - The remote-function kind being named.
|
|
25
|
+
* @param allKinds - Every kind generated for this operation, used to decide whether `query`,
|
|
26
|
+
* `command`, or `prerender` need a suffix.
|
|
27
|
+
*/
|
|
28
|
+
export function remoteFunctionName(baseName, kind, allKinds) {
|
|
29
|
+
// `query` and `command` are mutually exclusive under the default method-based resolution, but
|
|
30
|
+
// an explicit extension override can list both for the same operation. In that case leaving
|
|
31
|
+
// both bare would produce two identically-named exports, so only the sole survivor of the pair
|
|
32
|
+
// stays unsuffixed.
|
|
33
|
+
if (kind === "query" || kind === "command") {
|
|
34
|
+
const otherPrimaryKind = kind === "query" ? "command" : "query";
|
|
35
|
+
if (!allKinds.includes(otherPrimaryKind)) {
|
|
36
|
+
return baseName;
|
|
37
|
+
}
|
|
38
|
+
return `${baseName}${suffixByKind[kind]}`;
|
|
39
|
+
}
|
|
40
|
+
if (kind === "prerender" && allKinds.length === 1) {
|
|
41
|
+
return baseName;
|
|
42
|
+
}
|
|
43
|
+
return `${baseName}${suffixByKind[kind]}`;
|
|
44
|
+
}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Options, type PluginSveltekitRemoteFunctions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generates SvelteKit remote functions (`query`, `query.batch`, `query.live`, `command`, `form`,
|
|
4
|
+
* `prerender`) from an OpenAPI spec, one per operation, wrapping the async function
|
|
5
|
+
* `@kubb/plugin-fetch` already generates for that operation.
|
|
6
|
+
*
|
|
7
|
+
* The HTTP method decides which remote-function kind(s) an operation generates by default
|
|
8
|
+
* (`GET` → `query`; `POST | PUT | PATCH | DELETE` → `command`, plus `form` when
|
|
9
|
+
* `sveltekit.generateForms` is `true`), overridable per operation with the
|
|
10
|
+
* `x-sveltekit-remote-function-type` OpenAPI extension (an array of
|
|
11
|
+
* `'query' | 'query.batch' | 'query.live' | 'command' | 'form' | 'prerender'`), which always takes
|
|
12
|
+
* precedence over `sveltekit.generateForms`.
|
|
13
|
+
*
|
|
14
|
+
* Requires `pluginFetch()` and `pluginZod()` in the same pipeline (both are hard, driver-enforced
|
|
15
|
+
* dependencies). Every generated remote function uses the operation's `@kubb/plugin-zod`
|
|
16
|
+
* "options" schema directly as its argument schema, which requires two additional configuration
|
|
17
|
+
* requirements on those plugins:
|
|
18
|
+
* - `pluginZod({ inferred: true })` — `@kubb/plugin-zod` only generates the combined options
|
|
19
|
+
* schema in that mode.
|
|
20
|
+
* - `pluginFetch({ validator: { request: 'zod', response: 'zod' } })` — both request and response
|
|
21
|
+
* must be Zod-validated (the bare `validator: 'zod'` shorthand only validates the response).
|
|
22
|
+
*
|
|
23
|
+
* When either requirement isn't met, this plugin warns and skips generating any `*.remote.ts`
|
|
24
|
+
* files, without failing the rest of the build.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { defineConfig } from 'kubb/config'
|
|
29
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
30
|
+
* import { pluginZod } from '@kubb/plugin-zod'
|
|
31
|
+
* import { pluginFetch } from '@kubb/plugin-fetch'
|
|
32
|
+
* import { pluginSveltekitRemoteFunctions } from 'kubb-plugin-sveltekit-remote-functions'
|
|
33
|
+
*
|
|
34
|
+
* export default defineConfig({
|
|
35
|
+
* input: './petStore.yaml',
|
|
36
|
+
* output: { path: './src/gen' },
|
|
37
|
+
* plugins: [
|
|
38
|
+
* // Required by `pluginFetch()` itself (a hard dependency of its own) — not by this plugin.
|
|
39
|
+
* pluginTs(),
|
|
40
|
+
* pluginZod({ inferred: true }),
|
|
41
|
+
* pluginFetch({ output: { path: './clients' }, validator: { request: 'zod', response: 'zod' } }),
|
|
42
|
+
* pluginSveltekitRemoteFunctions({ output: { path: '../remote' } }),
|
|
43
|
+
* ],
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare const pluginSveltekitRemoteFunctions: (options?: Options | undefined) => import("kubb/kit").Plugin<PluginSveltekitRemoteFunctions>;
|
|
48
|
+
export default pluginSveltekitRemoteFunctions;
|