Skip to content

Commit 63ca464

Browse files
author
Codex
committed
fix(app): make openapi-effect a drop-in openapi-fetch wrapper
1 parent 9a993a2 commit 63ca464

7 files changed

Lines changed: 143 additions & 97 deletions

File tree

package.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
"packages/*"
99
],
1010
"scripts": {
11-
"build": "pnpm --filter @effect-template/app build",
12-
"check": "pnpm --filter @effect-template/app check",
11+
"build": "pnpm --filter openapi-effect build",
12+
"check": "pnpm --filter openapi-effect check",
1313
"changeset": "changeset",
1414
"changeset-publish": "node -e \"if (!process.env.NPM_TOKEN) { console.log('Skipping publish: NPM_TOKEN is not set'); process.exit(0); }\" && changeset publish",
1515
"changeset-version": "changeset version",
16-
"dev": "pnpm --filter @effect-template/app dev",
17-
"lint": "pnpm --filter @effect-template/app lint",
18-
"lint:tests": "pnpm --filter @effect-template/app lint:tests",
19-
"lint:effect": "pnpm --filter @effect-template/app lint:effect",
20-
"test": "pnpm --filter @effect-template/app test",
21-
"typecheck": "pnpm --filter @effect-template/app typecheck",
22-
"start": "pnpm --filter @effect-template/app start"
16+
"dev": "pnpm --filter openapi-effect dev",
17+
"lint": "pnpm --filter openapi-effect lint",
18+
"lint:tests": "pnpm --filter openapi-effect lint:tests",
19+
"lint:effect": "pnpm --filter openapi-effect lint:effect",
20+
"test": "pnpm --filter openapi-effect test",
21+
"typecheck": "pnpm --filter openapi-effect typecheck",
22+
"start": "pnpm --filter openapi-effect start"
2323
},
2424
"devDependencies": {
2525
"@changesets/changelog-github": "^0.5.2",

packages/app/package.json

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,31 @@
11
{
2-
"name": "@effect-template/app",
3-
"version": "1.0.12",
4-
"description": "Minimal Vite-powered TypeScript console starter using Effect",
5-
"main": "dist/main.js",
6-
"directories": {
7-
"doc": "doc"
2+
"name": "openapi-effect",
3+
"version": "0.0.0",
4+
"description": "Drop-in replacement for openapi-fetch with an opt-in Effect API",
5+
"type": "module",
6+
"main": "dist/index.js",
7+
"exports": {
8+
".": {
9+
"types": "./src/index.ts",
10+
"import": "./dist/index.js",
11+
"default": "./dist/index.js"
12+
}
813
},
914
"scripts": {
10-
"build": "vite build --ssr src/app/main.ts",
11-
"dev": "vite build --watch --ssr src/app/main.ts",
15+
"build": "vite build",
16+
"dev": "vite build --watch",
1217
"lint": "npx @ton-ai-core/vibecode-linter src/",
1318
"lint:tests": "npx @ton-ai-core/vibecode-linter tests/",
1419
"lint:effect": "npx eslint --config eslint.effect-ts-check.config.mjs .",
1520
"lint:types": "./scripts/lint-types.sh",
1621
"check": "pnpm run typecheck",
22+
"prepare": "pnpm run build",
1723
"prestart": "pnpm run build",
1824
"start": "node dist/main.js",
1925
"test": "pnpm run lint:tests && vitest run",
2026
"typecheck": "tsc --noEmit",
2127
"gen:strict-api": "npx ts-node --esm scripts/gen-strict-api.ts"
2228
},
23-
"repository": {
24-
"type": "git",
25-
"url": "git+https://github.com/ProverCoderAI/effect-template.git"
26-
},
27-
"keywords": [
28-
"effect",
29-
"typescript",
30-
"vite",
31-
"console"
32-
],
33-
"author": "",
34-
"license": "ISC",
35-
"type": "module",
36-
"bugs": {
37-
"url": "https://github.com/ProverCoderAI/effect-template/issues"
38-
},
39-
"homepage": "https://github.com/ProverCoderAI/effect-template#readme",
4029
"packageManager": "pnpm@10.28.2",
4130
"dependencies": {
4231
"@effect/cli": "^0.73.1",
@@ -52,6 +41,7 @@
5241
"@effect/typeclass": "^0.38.0",
5342
"@effect/workflow": "^0.16.0",
5443
"effect": "^3.19.15",
44+
"openapi-fetch": "^0.15.2",
5545
"openapi-typescript-helpers": "^0.0.15",
5646
"ts-morph": "^27.0.2"
5747
},

packages/app/src/index.ts

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,35 @@
1-
// CHANGE: Main entry point for openapi-effect package with Effect-native error handling
2-
// WHY: Enable default import of createClient function with proper error channel design
3-
// QUOTE(ТЗ): "import createClient from \"openapi-effect\""
4-
// REF: PR#3 comment from skulidropek about Effect representation
1+
// CHANGE: Make openapi-effect a drop-in replacement for openapi-fetch (Promise API), with an opt-in Effect API.
2+
// WHY: LeadForgeAI must be able to swap openapi-fetch -> openapi-effect with near-zero code changes.
3+
// QUOTE(ТЗ): "openapi-effect должен почти 1 в 1 заменяться с openapi-fetch" / "Просто добавлять effect поведение"
4+
// REF: user-msg-2026-02-12
55
// SOURCE: n/a
66
// PURITY: SHELL (re-exports)
77
// COMPLEXITY: O(1)
88

9-
// High-level API (recommended for most users)
10-
export { createClient as default } from "./shell/api-client/create-client.js"
9+
// Promise-based client (openapi-fetch compatible)
10+
export { default } from "openapi-fetch"
11+
export { default as createClient } from "openapi-fetch"
12+
export * from "openapi-fetch"
13+
14+
// Effect-based client (opt-in)
15+
export * as FetchHttpClient from "@effect/platform/FetchHttpClient"
16+
17+
// Strict Effect client (advanced)
18+
export type * from "./core/api-client/index.js"
19+
export { assertNever } from "./core/api-client/index.js"
20+
1121
export type {
12-
ClientOptions,
1322
DispatchersFor,
1423
StrictApiClient,
1524
StrictApiClientWithDispatchers
1625
} from "./shell/api-client/create-client.js"
17-
export { createClientEffect, registerDefaultDispatchers } from "./shell/api-client/create-client.js"
1826

19-
// Core types (for advanced type manipulation)
20-
// Effect Channel Design:
21-
// - ApiSuccess<Responses>: 2xx responses → success channel
22-
// - ApiFailure<Responses>: HttpError (4xx, 5xx) + BoundaryError → error channel
23-
export type {
24-
ApiFailure,
25-
ApiSuccess,
26-
BodyFor,
27-
BoundaryError,
28-
ContentTypesFor,
29-
DecodeError,
30-
HttpError,
31-
HttpErrorResponseVariant,
32-
HttpErrorVariants,
33-
OperationFor,
34-
ParseError,
35-
PathsForMethod,
36-
ResponsesFor,
37-
ResponseVariant,
38-
StatusCodes,
39-
SuccessVariants,
40-
TransportError,
41-
UnexpectedContentType,
42-
UnexpectedStatus
43-
} from "./core/api-client/index.js"
44-
45-
// Shell utilities (for custom implementations)
27+
export {
28+
createClient as createClientStrict,
29+
createClientEffect,
30+
registerDefaultDispatchers
31+
} from "./shell/api-client/create-client.js"
32+
4633
export type { Decoder, Dispatcher, RawResponse, StrictClient, StrictRequestInit } from "./shell/api-client/index.js"
4734

4835
export {

packages/app/src/shell/api-client/create-client-types.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import type * as HttpClient from "@effect/platform/HttpClient"
1212
import type { Effect } from "effect"
13+
import type { ClientOptions as OpenapiFetchClientOptions } from "openapi-fetch"
1314
import type { HttpMethod } from "openapi-typescript-helpers"
1415

1516
import type {
@@ -27,12 +28,7 @@ import type { Dispatcher } from "../../core/axioms.js"
2728
*
2829
* @pure - immutable configuration
2930
*/
30-
export type ClientOptions = {
31-
readonly baseUrl: string
32-
readonly credentials?: RequestCredentials
33-
readonly headers?: HeadersInit
34-
readonly fetch?: typeof globalThis.fetch
35-
}
31+
export type ClientOptions = OpenapiFetchClientOptions
3632

3733
// CHANGE: Add dispatcher map type for auto-dispatching clients
3834
// WHY: Enable creating clients that infer dispatcher from path+method without per-call parameter

packages/app/src/shell/api-client/create-client.ts

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const resolveDefaultDispatchers = <Paths extends object>(): DispatchersFor<Paths
9292
* @complexity O(n + m) where n = |params|, m = |query|
9393
*/
9494
const buildUrl = (
95-
baseUrl: string,
95+
baseUrl: string | undefined,
9696
path: string,
9797
params?: Record<string, ParamValue>,
9898
query?: Record<string, QueryValue>
@@ -105,23 +105,32 @@ const buildUrl = (
105105
}
106106
}
107107

108-
// Construct full URL
109-
const fullUrl = new URL(url, baseUrl)
110-
111-
// Add query parameters
108+
// Add query parameters without requiring an absolute baseUrl
112109
if (query) {
110+
const search = new URLSearchParams()
113111
for (const [key, value] of Object.entries(query)) {
114112
if (Array.isArray(value)) {
115113
for (const item of value) {
116-
fullUrl.searchParams.append(key, String(item))
114+
search.append(key, String(item))
117115
}
118116
} else {
119-
fullUrl.searchParams.set(key, String(value))
117+
search.set(key, String(value))
120118
}
121119
}
120+
121+
const queryString = search.toString()
122+
if (queryString.length > 0) {
123+
url = url.includes("?") ? `${url}&${queryString}` : `${url}?${queryString}`
124+
}
122125
}
123126

124-
return fullUrl.toString()
127+
// If no baseUrl is provided, keep the URL relative (browser-compatible)
128+
if (baseUrl === undefined || baseUrl === "") {
129+
return url
130+
}
131+
132+
// Resolve relative paths against baseUrl (Node-compatible)
133+
return new URL(url, baseUrl).toString()
125134
}
126135

127136
/**
@@ -174,16 +183,50 @@ const needsJsonContentType = (body: BodyInit | object | undefined): boolean =>
174183
* @pure true
175184
* @complexity O(n) where n = number of headers
176185
*/
186+
type HeaderOption = ClientOptions["headers"] | undefined
187+
188+
const toHeaders = (input: HeaderOption): Headers => {
189+
const headers = new Headers()
190+
191+
if (input === undefined) {
192+
return headers
193+
}
194+
195+
if (input instanceof Headers) {
196+
return new Headers(input)
197+
}
198+
199+
if (Array.isArray(input)) {
200+
for (const pair of input) {
201+
if (Array.isArray(pair) && pair.length === 2) {
202+
headers.set(String(pair[0]), String(pair[1]))
203+
}
204+
}
205+
return headers
206+
}
207+
208+
for (const [key, value] of Object.entries(input)) {
209+
if (value === undefined || value === null) {
210+
continue
211+
}
212+
if (Array.isArray(value)) {
213+
headers.set(key, value.map(String).join(","))
214+
continue
215+
}
216+
headers.set(key, String(value))
217+
}
218+
219+
return headers
220+
}
221+
177222
const mergeHeaders = (
178-
clientHeaders: HeadersInit | undefined,
179-
requestHeaders: HeadersInit | undefined
223+
clientHeaders: HeaderOption,
224+
requestHeaders: HeaderOption
180225
): Headers => {
181-
const headers = new Headers(clientHeaders)
182-
if (requestHeaders) {
183-
const optHeaders = new Headers(requestHeaders)
184-
for (const [key, value] of optHeaders.entries()) {
185-
headers.set(key, value)
186-
}
226+
const headers = toHeaders(clientHeaders)
227+
const optHeaders = toHeaders(requestHeaders)
228+
for (const [key, value] of optHeaders.entries()) {
229+
headers.set(key, value)
187230
}
188231
return headers
189232
}
@@ -197,7 +240,7 @@ type MethodHandlerOptions = {
197240
params?: Record<string, ParamValue> | undefined
198241
query?: Record<string, QueryValue> | undefined
199242
body?: BodyInit | object | undefined
200-
headers?: HeadersInit | undefined
243+
headers?: ClientOptions["headers"] | undefined
201244
signal?: AbortSignal | undefined
202245
}
203246

packages/app/vite.config.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from "node:fs"
12
import path from "node:path"
23
import { fileURLToPath } from "node:url"
34
import { defineConfig } from "vite"
@@ -6,6 +7,24 @@ import tsconfigPaths from "vite-tsconfig-paths"
67
const __filename = fileURLToPath(import.meta.url)
78
const __dirname = path.dirname(__filename)
89

10+
type Pkg = {
11+
dependencies?: Record<string, string> | undefined
12+
peerDependencies?: Record<string, string> | undefined
13+
}
14+
15+
// CHANGE: Build both the library entry (src/index.ts) and the CLI entry (src/app/main.ts).
16+
// WHY: Consumers need a JS entrypoint in dist for `import "openapi-effect"`, while we keep the template CLI working.
17+
// SOURCE: n/a
18+
const pkgPath = path.resolve(__dirname, "package.json")
19+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as Pkg
20+
const dependencies = [
21+
...Object.keys(pkg.dependencies ?? {}),
22+
...Object.keys(pkg.peerDependencies ?? {})
23+
]
24+
25+
const isExternal = (id: string): boolean =>
26+
dependencies.some((dep) => id === dep || id.startsWith(`${dep}/`))
27+
928
export default defineConfig({
1029
plugins: [tsconfigPaths()],
1130
publicDir: false,
@@ -18,15 +37,16 @@ export default defineConfig({
1837
target: "node20",
1938
outDir: "dist",
2039
sourcemap: true,
21-
ssr: "src/app/main.ts",
2240
rollupOptions: {
41+
input: {
42+
index: path.resolve(__dirname, "src/index.ts"),
43+
main: path.resolve(__dirname, "src/app/main.ts")
44+
},
45+
external: isExternal,
2346
output: {
2447
format: "es",
25-
entryFileNames: "main.js"
48+
entryFileNames: "[name].js"
2649
}
2750
}
28-
},
29-
ssr: {
30-
target: "node"
3151
}
3252
})

pnpm-lock.yaml

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)