@shiftapi/vite-plugin 0.0.1

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 ADDED
@@ -0,0 +1,82 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/fcjr/shiftapi/main/assets/logo.svg" alt="ShiftAPI Logo">
3
+ </p>
4
+
5
+ # @shiftapi/vite-plugin
6
+
7
+ Vite plugin that generates fully-typed TypeScript clients from [ShiftAPI](https://github.com/fcjr/shiftapi) Go servers. Get end-to-end type safety between your Go API and your frontend with zero manual type definitions.
8
+
9
+ ## How it works
10
+
11
+ 1. Extracts the OpenAPI 3.1 spec from your Go server at build time
12
+ 2. Generates TypeScript types using `openapi-typescript`
13
+ 3. Provides a virtual `@shiftapi/client` module with a pre-configured, fully-typed API client
14
+ 4. In dev mode, watches `.go` files and regenerates types on changes
15
+ 5. Auto-configures Vite's dev server proxy to forward API requests to your Go server
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install -D @shiftapi/vite-plugin
21
+ # or
22
+ pnpm add -D @shiftapi/vite-plugin
23
+ ```
24
+
25
+ **Peer dependency:** `vite` (v5 or v6).
26
+
27
+ ## Setup
28
+
29
+ ### vite.config.ts
30
+
31
+ ```ts
32
+ import { defineConfig } from "vite";
33
+ import shiftapi from "@shiftapi/vite-plugin";
34
+
35
+ export default defineConfig({
36
+ plugins: [
37
+ shiftapi({
38
+ server: "./cmd/server",
39
+ }),
40
+ ],
41
+ });
42
+ ```
43
+
44
+ ### Options
45
+
46
+ | Option | Type | Default | Description |
47
+ |--------|------|---------|-------------|
48
+ | `server` | `string` | **(required)** | Path to the Go server entry point (e.g. `"./cmd/server"` or `"./cmd/server/main.go"`) |
49
+ | `baseUrl` | `string` | `"/"` | Fallback base URL for the API client. Can be overridden via the `VITE_SHIFTAPI_BASE_URL` env var. |
50
+ | `goRoot` | `string` | `process.cwd()` | Working directory for `go run` |
51
+ | `url` | `string` | `"http://localhost:8080"` | Address the Go server listens on. Used to auto-configure the Vite dev proxy. |
52
+
53
+ ## Usage
54
+
55
+ Import the typed client in your frontend code:
56
+
57
+ ```ts
58
+ import { client } from "@shiftapi/client";
59
+
60
+ const { data, error } = await client.GET("/greet", {
61
+ params: { query: { name: "World" } },
62
+ });
63
+ // `data` and `error` are fully typed based on your Go handler signatures
64
+ ```
65
+
66
+ The `createClient` factory is also exported if you need a custom instance:
67
+
68
+ ```ts
69
+ import { createClient } from "@shiftapi/client";
70
+
71
+ const api = createClient({ baseUrl: "https://api.example.com" });
72
+ ```
73
+
74
+ ## What it auto-configures
75
+
76
+ - **Vite proxy** -- API paths discovered from your OpenAPI spec are automatically proxied to your Go server during development.
77
+ - **tsconfig.json** -- A path mapping for `@shiftapi/client` is added so TypeScript resolves the generated types.
78
+ - **HMR** -- When `.go` files change, the plugin restarts the Go server, regenerates types, and triggers a full reload in the browser.
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,22 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ interface ShiftAPIPluginOptions {
4
+ /** Path to the Go server entry point (e.g., "./cmd/server" or "./cmd/server/main.go") */
5
+ server: string;
6
+ /**
7
+ * Fallback base URL for the API client (default: "/").
8
+ * Can be overridden at build time via the `VITE_SHIFTAPI_BASE_URL` env var.
9
+ */
10
+ baseUrl?: string;
11
+ /** Working directory for `go run` (default: process.cwd()) */
12
+ goRoot?: string;
13
+ /**
14
+ * Address the Go server listens on (default: "http://localhost:8080").
15
+ * Used to auto-configure the Vite proxy in dev mode.
16
+ */
17
+ url?: string;
18
+ }
19
+
20
+ declare function shiftapiPlugin(options: ShiftAPIPluginOptions): Plugin;
21
+
22
+ export { type ShiftAPIPluginOptions, shiftapiPlugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,239 @@
1
+ // src/index.ts
2
+ import { resolve, relative } from "path";
3
+ import {
4
+ writeFileSync,
5
+ readFileSync as readFileSync2,
6
+ mkdirSync,
7
+ existsSync
8
+ } from "fs";
9
+ import { spawn } from "child_process";
10
+
11
+ // src/extract.ts
12
+ import { execFileSync } from "child_process";
13
+ import { readFileSync, unlinkSync, rmSync, mkdtempSync } from "fs";
14
+ import { join } from "path";
15
+ import { tmpdir } from "os";
16
+ function extractSpec(serverEntry, goRoot) {
17
+ const tempDir = mkdtempSync(join(tmpdir(), "shiftapi-"));
18
+ const specPath = join(tempDir, "openapi.json");
19
+ try {
20
+ execFileSync("go", ["run", serverEntry], {
21
+ cwd: goRoot,
22
+ env: {
23
+ ...process.env,
24
+ SHIFTAPI_EXPORT_SPEC: specPath
25
+ },
26
+ stdio: ["ignore", "pipe", "pipe"],
27
+ timeout: 3e4
28
+ });
29
+ } catch (err) {
30
+ const stderr = err instanceof Error && "stderr" in err ? String(err.stderr) : "";
31
+ throw new Error(
32
+ `@shiftapi/vite-plugin: Failed to extract OpenAPI spec.
33
+ Command: go run ${serverEntry}
34
+ CWD: ${goRoot}
35
+ Error: ${stderr || String(err)}`
36
+ );
37
+ }
38
+ let raw;
39
+ try {
40
+ raw = readFileSync(specPath, "utf-8");
41
+ } catch {
42
+ throw new Error(
43
+ `@shiftapi/vite-plugin: Spec file was not created at ${specPath}.
44
+ Make sure your Go server calls shiftapi.ListenAndServe().`
45
+ );
46
+ }
47
+ try {
48
+ unlinkSync(specPath);
49
+ rmSync(tempDir, { recursive: true });
50
+ } catch {
51
+ }
52
+ return JSON.parse(raw);
53
+ }
54
+
55
+ // src/generate.ts
56
+ import openapiTS, { astToString } from "openapi-typescript";
57
+ async function generateTypes(spec) {
58
+ const ast = await openapiTS(spec);
59
+ return astToString(ast);
60
+ }
61
+
62
+ // src/virtualModule.ts
63
+ function buildVirtualModuleSource(baseUrl) {
64
+ return `// Auto-generated by @shiftapi/vite-plugin
65
+ import createClient from "openapi-fetch";
66
+
67
+ /** Pre-configured, fully-typed API client. */
68
+ export const client = createClient({
69
+ baseUrl: import.meta.env.VITE_SHIFTAPI_BASE_URL || ${JSON.stringify(baseUrl)},
70
+ });
71
+
72
+ export { createClient };
73
+ `;
74
+ }
75
+
76
+ // src/index.ts
77
+ var MODULE_ID = "@shiftapi/client";
78
+ var RESOLVED_MODULE_ID = "\0" + MODULE_ID;
79
+ function shiftapiPlugin(options) {
80
+ const {
81
+ server: serverEntry,
82
+ baseUrl = "/",
83
+ goRoot = process.cwd(),
84
+ url = "http://localhost:8080"
85
+ } = options;
86
+ let virtualModuleSource = "";
87
+ let generatedDts = "";
88
+ let cachedSpec = null;
89
+ let devServer;
90
+ let goProcess = null;
91
+ let debounceTimer = null;
92
+ let projectRoot = process.cwd();
93
+ function getSpec() {
94
+ if (!cachedSpec) {
95
+ cachedSpec = extractSpec(
96
+ serverEntry,
97
+ resolve(goRoot)
98
+ );
99
+ }
100
+ return cachedSpec;
101
+ }
102
+ async function regenerate() {
103
+ cachedSpec = null;
104
+ const spec = getSpec();
105
+ const types = await generateTypes(spec);
106
+ if (types === generatedDts) {
107
+ return false;
108
+ }
109
+ generatedDts = types;
110
+ virtualModuleSource = buildVirtualModuleSource(baseUrl);
111
+ return true;
112
+ }
113
+ function writeDtsFile() {
114
+ const outDir = resolve("node_modules", ".shiftapi");
115
+ if (!existsSync(outDir)) {
116
+ mkdirSync(outDir, { recursive: true });
117
+ }
118
+ const dtsContent = `// Auto-generated by @shiftapi/vite-plugin. Do not edit.
119
+ declare module "@shiftapi/client" {
120
+ ${generatedDts.split("\n").map((line) => line ? " " + line : line).join("\n")}
121
+
122
+ import type createClient from "openapi-fetch";
123
+
124
+ export const client: ReturnType<typeof createClient<paths>>;
125
+ export { createClient };
126
+ }
127
+ `;
128
+ writeFileSync(resolve(outDir, "client.d.ts"), dtsContent);
129
+ }
130
+ function patchTsConfig() {
131
+ const tsconfigPath = resolve(projectRoot, "tsconfig.json");
132
+ if (!existsSync(tsconfigPath)) return;
133
+ const raw = readFileSync2(tsconfigPath, "utf-8");
134
+ const tsconfig = JSON.parse(raw);
135
+ if (tsconfig?.compilerOptions?.paths?.[MODULE_ID]) return;
136
+ if (!tsconfig.compilerOptions) tsconfig.compilerOptions = {};
137
+ if (!tsconfig.compilerOptions.paths) tsconfig.compilerOptions.paths = {};
138
+ tsconfig.compilerOptions.paths[MODULE_ID] = [
139
+ "./node_modules/.shiftapi/client.d.ts"
140
+ ];
141
+ const indent = raw.match(/^[ \t]+/m)?.[0] ?? " ";
142
+ writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, indent) + "\n");
143
+ console.log(
144
+ "[shiftapi] Updated tsconfig.json with @shiftapi/client path mapping"
145
+ );
146
+ }
147
+ function startGoServer() {
148
+ goProcess = spawn("go", ["run", serverEntry], {
149
+ cwd: resolve(goRoot),
150
+ stdio: ["ignore", "inherit", "inherit"]
151
+ });
152
+ goProcess.on("error", (err) => {
153
+ console.error("[shiftapi] Failed to start Go server:", err.message);
154
+ });
155
+ goProcess.on("exit", (code) => {
156
+ if (code !== null && code !== 0) {
157
+ console.error(`[shiftapi] Go server exited with code ${code}`);
158
+ }
159
+ goProcess = null;
160
+ });
161
+ console.log(`[shiftapi] Go server starting: go run ${serverEntry}`);
162
+ }
163
+ function stopGoServer() {
164
+ if (goProcess) {
165
+ goProcess.kill();
166
+ goProcess = null;
167
+ }
168
+ }
169
+ return {
170
+ name: "@shiftapi/vite-plugin",
171
+ configResolved(config) {
172
+ projectRoot = config.root;
173
+ patchTsConfig();
174
+ },
175
+ config() {
176
+ const spec = getSpec();
177
+ const paths = spec.paths;
178
+ if (!paths) return;
179
+ const proxy = {};
180
+ for (const path of Object.keys(paths)) {
181
+ proxy[path] = url;
182
+ }
183
+ return {
184
+ server: { proxy }
185
+ };
186
+ },
187
+ configureServer(server) {
188
+ devServer = server;
189
+ server.watcher.add(resolve(goRoot));
190
+ startGoServer();
191
+ server.httpServer?.on("close", stopGoServer);
192
+ },
193
+ async buildStart() {
194
+ await regenerate();
195
+ writeDtsFile();
196
+ },
197
+ resolveId(id) {
198
+ if (id === MODULE_ID) {
199
+ return RESOLVED_MODULE_ID;
200
+ }
201
+ },
202
+ load(id) {
203
+ if (id === RESOLVED_MODULE_ID) {
204
+ return virtualModuleSource;
205
+ }
206
+ },
207
+ async handleHotUpdate({ file }) {
208
+ const resolvedGoRoot = resolve(goRoot);
209
+ if (!file.endsWith(".go") || !file.startsWith(resolvedGoRoot)) {
210
+ return;
211
+ }
212
+ console.log(
213
+ `[shiftapi] Go file changed: ${relative(resolvedGoRoot, file)}`
214
+ );
215
+ if (debounceTimer) clearTimeout(debounceTimer);
216
+ debounceTimer = setTimeout(async () => {
217
+ try {
218
+ stopGoServer();
219
+ startGoServer();
220
+ const changed = await regenerate();
221
+ if (changed && devServer) {
222
+ writeDtsFile();
223
+ const mod = devServer.moduleGraph.getModuleById(RESOLVED_MODULE_ID);
224
+ if (mod) {
225
+ devServer.moduleGraph.invalidateModule(mod);
226
+ devServer.ws.send({ type: "full-reload" });
227
+ }
228
+ console.log("[shiftapi] Types regenerated.");
229
+ }
230
+ } catch (err) {
231
+ console.error("[shiftapi] Failed to regenerate:", err);
232
+ }
233
+ }, 500);
234
+ }
235
+ };
236
+ }
237
+ export {
238
+ shiftapiPlugin as default
239
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@shiftapi/vite-plugin",
3
+ "version": "0.0.1",
4
+ "description": "Vite plugin for fully-typed TypeScript clients from shiftapi Go servers",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./virtual": {
14
+ "types": "./virtual.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "virtual.d.ts"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup src/index.ts --format esm --dts",
23
+ "dev": "tsup src/index.ts --format esm --dts --watch",
24
+ "test": "vitest run"
25
+ },
26
+ "peerDependencies": {
27
+ "vite": "^5.0.0 || ^6.0.0"
28
+ },
29
+ "dependencies": {
30
+ "openapi-fetch": "^0.12.0 || ^0.13.0",
31
+ "openapi-typescript": "^7.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^25.2.3",
35
+ "openapi-fetch": "^0.13.0",
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.5.0",
38
+ "vite": "^6.0.0",
39
+ "vitest": "^2.0.0"
40
+ }
41
+ }
package/virtual.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ // Fallback type declaration for the @shiftapi/client virtual module.
2
+ // Run `vite dev` or `vite build` to generate the real API-specific types.
3
+ declare module "@shiftapi/client" {
4
+ import type createClient from "openapi-fetch";
5
+
6
+ /** OpenAPI paths type — generated from your Go API */
7
+ export type paths = Record<string, any>;
8
+ /** OpenAPI components type — generated from your Go API */
9
+ export type components = Record<string, any>;
10
+ /** OpenAPI operations type — generated from your Go API */
11
+ export type operations = Record<string, any>;
12
+
13
+ /** Pre-configured, fully-typed API client */
14
+ export const client: ReturnType<typeof createClient<paths>>;
15
+ export { createClient };
16
+ }