Skip to content

Commit 06b665f

Browse files
committed
refactor: move createFetch into graphql-query/runtime, drop @constructive-io/fetch dep
Instead of adding @constructive-io/fetch as an external dependency to every SDK package, embed the localhost-aware fetch directly in @constructive-io/graphql-query/runtime — which every generated client already imports. Zero new dependencies for consumers. - Added graphql/query/src/runtime/localhost-fetch.ts - Re-exported createFetch from runtime/index.ts - Updated codegen template to import from runtime - Removed @constructive-io/fetch from all SDK + server-test package.json - Regenerated all 13 SDK client files
1 parent 1be22a2 commit 06b665f

25 files changed

Lines changed: 222 additions & 90 deletions

File tree

graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,12 @@ exports[`client-generator generateOrmClientFile generates OrmClient class with e
104104
* @generated by @constructive-io/graphql-codegen
105105
* DO NOT EDIT - changes will be overwritten
106106
*/
107-
import { createFetch } from '@constructive-io/fetch';
108107
import type {
109108
GraphQLAdapter,
110109
GraphQLError,
111110
QueryResult,
112111
} from '@constructive-io/graphql-query/runtime';
112+
import { createFetch } from '@constructive-io/graphql-query/runtime';
113113
114114
export type {
115115
GraphQLAdapter,
@@ -205,9 +205,10 @@ export interface OrmClientConfig {
205205
/** Default headers for HTTP requests (only used with endpoint) */
206206
headers?: Record<string, string>;
207207
/**
208-
* Custom fetch implementation. Defaults to @constructive-io/fetch
209-
* which handles *.localhost DNS and Host headers in Node.js.
210-
* Pass your own for test mocking or custom proxy/credentials.
208+
* Custom fetch implementation. Defaults to createFetch() from
209+
* @constructive-io/graphql-query/runtime which handles *.localhost
210+
* DNS and Host headers in Node.js. Pass your own for test mocking
211+
* or custom proxy/credentials.
211212
*/
212213
fetch?: typeof globalThis.fetch;
213214
/** Custom adapter for GraphQL execution (overrides endpoint/headers/fetch) */

graphql/codegen/src/__tests__/codegen/client-generator.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@ describe('client-generator', () => {
6868
expect(result.content).toContain('config.fetch');
6969
});
7070

71-
it('imports createFetch from @constructive-io/fetch', () => {
71+
it('imports createFetch from @constructive-io/graphql-query/runtime', () => {
7272
const result = generateOrmClientFile();
7373

7474
expect(result.content).toContain(
75-
"import { createFetch } from '@constructive-io/fetch'",
75+
"import { createFetch } from '@constructive-io/graphql-query/runtime'",
7676
);
7777
expect(result.content).toContain('createFetch()');
7878
});

graphql/codegen/src/core/codegen/templates/orm-client.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
* Any changes here will affect all generated ORM clients.
99
*/
1010

11-
import { createFetch } from '@constructive-io/fetch';
1211
import type {
1312
GraphQLAdapter,
1413
GraphQLError,
1514
QueryResult,
1615
} from '@constructive-io/graphql-query/runtime';
16+
import { createFetch } from '@constructive-io/graphql-query/runtime';
1717

1818
export type {
1919
GraphQLAdapter,
@@ -109,9 +109,10 @@ export interface OrmClientConfig {
109109
/** Default headers for HTTP requests (only used with endpoint) */
110110
headers?: Record<string, string>;
111111
/**
112-
* Custom fetch implementation. Defaults to @constructive-io/fetch
113-
* which handles *.localhost DNS and Host headers in Node.js.
114-
* Pass your own for test mocking or custom proxy/credentials.
112+
* Custom fetch implementation. Defaults to createFetch() from
113+
* @constructive-io/graphql-query/runtime which handles *.localhost
114+
* DNS and Host headers in Node.js. Pass your own for test mocking
115+
* or custom proxy/credentials.
115116
*/
116117
fetch?: typeof globalThis.fetch;
117118
/** Custom adapter for GraphQL execution (overrides endpoint/headers/fetch) */

graphql/query/src/runtime/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
* Runtime sub-export for generated ORM code.
33
*
44
* Generated ORM clients need runtime dependencies at execution time.
5-
* This module re-exports two of the three so generated code can consolidate imports:
5+
* This module re-exports so generated code can consolidate imports:
66
* - @0no-co/graphql.web — parseType, print
77
* - @constructive-io/graphql-types — GraphQLAdapter, GraphQLError, QueryResult
8+
* - ./localhost-fetch — createFetch (isomorphic *.localhost-aware fetch)
89
*
910
* gql-ast is intentionally NOT re-exported here because the templates
1011
* use `import * as t from 'gql-ast'` — mixing it into this namespace
1112
* would pollute `t` with unrelated symbols like parseType and print.
1213
*
1314
* Usage in generated templates:
14-
* import { parseType, print } from '@constructive-io/graphql-query/runtime';
15+
* import { parseType, print, createFetch } from '@constructive-io/graphql-query/runtime';
1516
* import * as t from 'gql-ast';
1617
* import type { GraphQLAdapter } from '@constructive-io/graphql-query/runtime';
1718
*/
@@ -21,3 +22,7 @@ export { parseType, print } from '@0no-co/graphql.web';
2122

2223
// From @constructive-io/graphql-types — adapter interface + result types
2324
export type { GraphQLAdapter, GraphQLError, QueryResult } from '@constructive-io/graphql-types';
25+
26+
// Isomorphic fetch with *.localhost DNS + Host header fix for Node.js
27+
export { createFetch } from './localhost-fetch';
28+
export type { FetchFunction } from './localhost-fetch';
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* Isomorphic fetch that resolves *.localhost subdomains and preserves
3+
* Host headers across Node.js and browsers.
4+
*
5+
* Node.js has two issues with *.localhost subdomains:
6+
* 1. DNS — fetch('http://auth.localhost:3000/') throws ENOTFOUND
7+
* because undici doesn't resolve *.localhost to loopback.
8+
* 2. Host header — Node's fetch treats Host as forbidden and silently
9+
* drops it, breaking server-side subdomain routing.
10+
*
11+
* In browsers *.localhost resolves natively, so createFetch() returns
12+
* globalThis.fetch as-is.
13+
*/
14+
15+
export type FetchFunction = typeof globalThis.fetch;
16+
17+
export function isLocalhostSubdomain(hostname: string): boolean {
18+
return hostname.endsWith('.localhost') && hostname !== 'localhost';
19+
}
20+
21+
function buildNodeFetch(
22+
http: typeof import('node:http'),
23+
https: typeof import('node:https'),
24+
): FetchFunction {
25+
return (input, init) => {
26+
const url = new URL(
27+
typeof input === 'string'
28+
? input
29+
: input instanceof URL
30+
? input.href
31+
: input.url,
32+
);
33+
34+
if (!isLocalhostSubdomain(url.hostname)) {
35+
return globalThis.fetch(input, init);
36+
}
37+
38+
const originalHost = url.host;
39+
url.hostname = 'localhost';
40+
41+
return new Promise((resolve, reject) => {
42+
const headers: Record<string, string> = {
43+
Host: originalHost,
44+
};
45+
46+
if (init?.headers) {
47+
const entries =
48+
init.headers instanceof Headers
49+
? Array.from(init.headers.entries())
50+
: Array.isArray(init.headers)
51+
? init.headers
52+
: Object.entries(init.headers);
53+
for (const [key, value] of entries) {
54+
headers[key] = value;
55+
}
56+
}
57+
58+
const protocol = url.protocol === 'https:' ? https : http;
59+
60+
const req = protocol.request(
61+
url,
62+
{
63+
method: init?.method ?? 'GET',
64+
headers,
65+
},
66+
(res) => {
67+
const chunks: Buffer[] = [];
68+
res.on('data', (chunk: Buffer) => chunks.push(chunk));
69+
res.on('end', () => {
70+
const body = Buffer.concat(chunks);
71+
resolve(
72+
new Response(body, {
73+
status: res.statusCode ?? 0,
74+
statusText: res.statusMessage ?? '',
75+
headers: res.headers as Record<string, string>,
76+
}),
77+
);
78+
});
79+
},
80+
);
81+
82+
req.on('error', reject);
83+
84+
if (init?.signal) {
85+
const onAbort = () => {
86+
req.destroy(new Error('The operation was aborted'));
87+
};
88+
init.signal.addEventListener('abort', onAbort, { once: true });
89+
req.on('close', () => {
90+
init.signal!.removeEventListener('abort', onAbort);
91+
});
92+
}
93+
94+
if (init?.body != null) {
95+
req.write(
96+
typeof init.body === 'string' || init.body instanceof Uint8Array
97+
? init.body
98+
: String(init.body),
99+
);
100+
}
101+
102+
req.end();
103+
});
104+
};
105+
}
106+
107+
let _fetch: FetchFunction | undefined;
108+
109+
/**
110+
* Create an isomorphic fetch function.
111+
*
112+
* - In browsers (and Deno/Bun/edge): returns globalThis.fetch as-is.
113+
* - In Node.js: returns a wrapper that uses node:http/node:https for
114+
* *.localhost URLs (fixing DNS + Host header) and delegates everything
115+
* else to globalThis.fetch.
116+
*
117+
* The result is cached — calling createFetch() multiple times returns
118+
* the same function instance.
119+
*/
120+
export function createFetch(): FetchFunction {
121+
if (_fetch) return _fetch;
122+
123+
if (typeof process !== 'undefined' && process.versions?.node) {
124+
try {
125+
// eslint-disable-next-line @typescript-eslint/no-require-imports
126+
const http = require('node:http');
127+
// eslint-disable-next-line @typescript-eslint/no-require-imports
128+
const https = require('node:https');
129+
_fetch = buildNodeFetch(http, https);
130+
return _fetch;
131+
} catch {
132+
// node:http unavailable — fall through
133+
}
134+
}
135+
136+
_fetch = globalThis.fetch;
137+
return _fetch;
138+
}

graphql/server-test/__tests__/cli-e2e.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ function resolveNodePaths(): string[] {
5252
'inquirerer',
5353
'nested-obj',
5454
'graphql',
55-
'@constructive-io/fetch',
5655
'@constructive-io/graphql-types',
5756
'@constructive-io/graphql-query',
5857
'@agentic-kit/ollama',

graphql/server-test/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
"nested-obj": "*"
4444
},
4545
"dependencies": {
46-
"@constructive-io/fetch": "^1.0.0",
4746
"@constructive-io/graphql-env": "workspace:^",
4847
"@constructive-io/graphql-server": "workspace:^",
4948
"@constructive-io/graphql-types": "workspace:^",

pnpm-lock.yaml

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

sdk/constructive-cli/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
],
4747
"dependencies": {
4848
"@0no-co/graphql.web": "^1.1.2",
49-
"@constructive-io/fetch": "^1.0.0",
5049
"@constructive-io/graphql-query": "workspace:^",
5150
"@constructive-io/graphql-types": "workspace:^",
5251
"appstash": "^0.7.0",

sdk/constructive-cli/src/admin/orm/client.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
* @generated by @constructive-io/graphql-codegen
44
* DO NOT EDIT - changes will be overwritten
55
*/
6-
import { createFetch } from '@constructive-io/fetch';
76
import type {
87
GraphQLAdapter,
98
GraphQLError,
109
QueryResult,
1110
} from '@constructive-io/graphql-query/runtime';
11+
import { createFetch } from '@constructive-io/graphql-query/runtime';
1212

1313
export type {
1414
GraphQLAdapter,
@@ -104,9 +104,10 @@ export interface OrmClientConfig {
104104
/** Default headers for HTTP requests (only used with endpoint) */
105105
headers?: Record<string, string>;
106106
/**
107-
* Custom fetch implementation. Defaults to @constructive-io/fetch
108-
* which handles *.localhost DNS and Host headers in Node.js.
109-
* Pass your own for test mocking or custom proxy/credentials.
107+
* Custom fetch implementation. Defaults to createFetch() from
108+
* @constructive-io/graphql-query/runtime which handles *.localhost
109+
* DNS and Host headers in Node.js. Pass your own for test mocking
110+
* or custom proxy/credentials.
110111
*/
111112
fetch?: typeof globalThis.fetch;
112113
/** Custom adapter for GraphQL execution (overrides endpoint/headers/fetch) */

0 commit comments

Comments
 (0)