Skip to content

Commit 4902d86

Browse files
committed
feat: add dual build system with dynamic fetch wrapper
- Add fetchWrapper system with separate CJS/ESM implementations - Add setupFetchWrapper.js script for build-time wrapper selection - Update TypeScript configs to support dual module builds - Modify apiClient to use new fetch wrapper system - Update tests for new fetch handling approach This improves compatibility between CommonJS and ES modules by using dynamic imports for node-fetch in CJS builds while maintaining native fetch support in ESM builds.
1 parent 9578935 commit 4902d86

File tree

11 files changed

+222
-11
lines changed

11 files changed

+222
-11
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
"generate-model-index": "node scripts/generateModelIndex.js",
2727
"prebuild": "npm run export-version && npm run generate-model-index",
2828
"build": "rm -rf lib && npm run build-esm && npm run build-cjs && npm run generate-lib-package-json",
29-
"build-esm": "tsc -p tsconfig.esm.json",
30-
"build-cjs": "tsc -p tsconfig.cjs.json",
29+
"build-esm": "node scripts/setupFetchWrapper.js esm && tsc -p tsconfig.esm.json",
30+
"build-cjs": "node scripts/setupFetchWrapper.js cjs && tsc -p tsconfig.cjs.json",
3131
"prepare": "npm run build",
3232
"build:docs": "typedoc --out docs",
3333
"version": "npm run export-version && git add src/version.ts"

scripts/setupFetchWrapper.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Script to copy the appropriate fetchWrapper implementation based on build type
3+
*/
4+
5+
const fs = require('fs');
6+
const path = require('path');
7+
8+
const buildType = process.argv[2]; // 'esm' or 'cjs'
9+
10+
if (!buildType || !['esm', 'cjs'].includes(buildType)) {
11+
console.error('Usage: node setupFetchWrapper.js <esm|cjs>');
12+
process.exit(1);
13+
}
14+
15+
const srcDir = path.join(__dirname, '..', 'src', 'utils');
16+
const sourceFile = path.join(srcDir, `fetchWrapper-${buildType}.ts`);
17+
const targetFile = path.join(srcDir, 'fetchWrapper.ts');
18+
19+
// Ensure source file exists
20+
if (!fs.existsSync(sourceFile)) {
21+
console.error(`Source file ${sourceFile} does not exist`);
22+
process.exit(1);
23+
}
24+
25+
// Copy the appropriate implementation
26+
fs.copyFileSync(sourceFile, targetFile);
27+
/* eslint-disable no-console */
28+
console.log(`✅ Copied fetchWrapper-${buildType}.ts to fetchWrapper.ts`);
29+
/* eslint-enable no-console */

src/apiClient.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import fetch, { Request, Response } from 'node-fetch';
21
import { Readable as _Readable } from 'node:stream';
32
import { NylasConfig, OverridableNylasConfig } from './config.js';
43
import {
@@ -11,6 +10,8 @@ import { SDK_VERSION } from './version.js';
1110
import { FormData } from 'formdata-node';
1211
import { FormDataEncoder as _FormDataEncoder } from 'form-data-encoder';
1312
import { snakeCase } from 'change-case';
13+
import { getFetch, getRequest } from './utils/fetchWrapper.js';
14+
import type { Request, Response } from './utils/fetchWrapper.js';
1415

1516
/**
1617
* The header key for the debugging flow ID
@@ -155,7 +156,7 @@ export default class APIClient {
155156
}
156157

157158
private async sendRequest(options: RequestOptionsParams): Promise<Response> {
158-
const req = this.newRequest(options);
159+
const req = await this.newRequest(options);
159160
const controller: AbortController = new AbortController();
160161

161162
// Handle timeout
@@ -177,6 +178,7 @@ export default class APIClient {
177178
}, timeoutDuration);
178179

179180
try {
181+
const fetch = await getFetch();
180182
const response = await fetch(req, {
181183
signal: controller.signal as AbortSignal,
182184
});
@@ -271,9 +273,10 @@ export default class APIClient {
271273
return requestOptions;
272274
}
273275

274-
newRequest(options: RequestOptionsParams): Request {
276+
async newRequest(options: RequestOptionsParams): Promise<Request> {
275277
const newOptions = this.requestOptions(options);
276-
return new Request(newOptions.url, {
278+
const RequestConstructor = await getRequest();
279+
return new RequestConstructor(newOptions.url, {
277280
method: newOptions.method,
278281
headers: newOptions.headers,
279282
body: newOptions.body,

src/utils/fetchWrapper-cjs.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Fetch wrapper for CJS builds - uses dynamic imports for node-fetch compatibility
3+
*/
4+
5+
// Types for the dynamic import result
6+
interface NodeFetchModule {
7+
default: any; // fetch function
8+
Request: any; // Request constructor
9+
Response: any; // Response constructor
10+
}
11+
12+
// Cache for the dynamically imported module
13+
let nodeFetchModule: NodeFetchModule | null = null;
14+
15+
/**
16+
* Get fetch function - uses dynamic import for CJS
17+
*/
18+
export async function getFetch(): Promise<any> {
19+
// In test environment, use global fetch (mocked by jest-fetch-mock)
20+
if (typeof global !== 'undefined' && global.fetch) {
21+
return global.fetch;
22+
}
23+
24+
if (!nodeFetchModule) {
25+
// Use Function constructor to prevent TypeScript from converting to require()
26+
const dynamicImport = new Function('specifier', 'return import(specifier)');
27+
nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule;
28+
}
29+
30+
return nodeFetchModule.default;
31+
}
32+
33+
/**
34+
* Get Request constructor - uses dynamic import for CJS
35+
*/
36+
export async function getRequest(): Promise<any> {
37+
// In test environment, use global Request or a mock
38+
if (typeof global !== 'undefined' && global.Request) {
39+
return global.Request;
40+
}
41+
42+
if (!nodeFetchModule) {
43+
// Use Function constructor to prevent TypeScript from converting to require()
44+
const dynamicImport = new Function('specifier', 'return import(specifier)');
45+
nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule;
46+
}
47+
48+
return nodeFetchModule.Request;
49+
}
50+
51+
/**
52+
* Get Response constructor - uses dynamic import for CJS
53+
*/
54+
export async function getResponse(): Promise<any> {
55+
// In test environment, use global Response or a mock
56+
if (typeof global !== 'undefined' && global.Response) {
57+
return global.Response;
58+
}
59+
60+
if (!nodeFetchModule) {
61+
// Use Function constructor to prevent TypeScript from converting to require()
62+
const dynamicImport = new Function('specifier', 'return import(specifier)');
63+
nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule;
64+
}
65+
66+
return nodeFetchModule.Response;
67+
}
68+
69+
// Export types as any for CJS compatibility
70+
export type RequestInit = any;
71+
export type HeadersInit = any;
72+
export type Request = any;
73+
export type Response = any;

src/utils/fetchWrapper-esm.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Fetch wrapper for ESM builds - uses static imports for optimal performance
3+
*/
4+
5+
import fetch, { Request, Response } from 'node-fetch';
6+
import type { RequestInit, HeadersInit } from 'node-fetch';
7+
8+
/**
9+
* Get fetch function - uses static import for ESM
10+
*/
11+
export async function getFetch(): Promise<typeof fetch> {
12+
return fetch;
13+
}
14+
15+
/**
16+
* Get Request constructor - uses static import for ESM
17+
*/
18+
export async function getRequest(): Promise<typeof Request> {
19+
return Request;
20+
}
21+
22+
/**
23+
* Get Response constructor - uses static import for ESM
24+
*/
25+
export async function getResponse(): Promise<typeof Response> {
26+
return Response;
27+
}
28+
29+
// Export types directly
30+
export type { RequestInit, HeadersInit, Request, Response };

src/utils/fetchWrapper.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Fetch wrapper for CJS builds - uses dynamic imports for node-fetch compatibility
3+
*/
4+
5+
// Types for the dynamic import result
6+
interface NodeFetchModule {
7+
default: any; // fetch function
8+
Request: any; // Request constructor
9+
Response: any; // Response constructor
10+
}
11+
12+
// Cache for the dynamically imported module
13+
let nodeFetchModule: NodeFetchModule | null = null;
14+
15+
/**
16+
* Get fetch function - uses dynamic import for CJS
17+
*/
18+
export async function getFetch(): Promise<any> {
19+
// In test environment, use global fetch (mocked by jest-fetch-mock)
20+
if (typeof global !== 'undefined' && global.fetch) {
21+
return global.fetch;
22+
}
23+
24+
if (!nodeFetchModule) {
25+
// Use Function constructor to prevent TypeScript from converting to require()
26+
const dynamicImport = new Function('specifier', 'return import(specifier)');
27+
nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule;
28+
}
29+
30+
return nodeFetchModule.default;
31+
}
32+
33+
/**
34+
* Get Request constructor - uses dynamic import for CJS
35+
*/
36+
export async function getRequest(): Promise<any> {
37+
// In test environment, use global Request or a mock
38+
if (typeof global !== 'undefined' && global.Request) {
39+
return global.Request;
40+
}
41+
42+
if (!nodeFetchModule) {
43+
// Use Function constructor to prevent TypeScript from converting to require()
44+
const dynamicImport = new Function('specifier', 'return import(specifier)');
45+
nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule;
46+
}
47+
48+
return nodeFetchModule.Request;
49+
}
50+
51+
/**
52+
* Get Response constructor - uses dynamic import for CJS
53+
*/
54+
export async function getResponse(): Promise<any> {
55+
// In test environment, use global Response or a mock
56+
if (typeof global !== 'undefined' && global.Response) {
57+
return global.Response;
58+
}
59+
60+
if (!nodeFetchModule) {
61+
// Use Function constructor to prevent TypeScript from converting to require()
62+
const dynamicImport = new Function('specifier', 'return import(specifier)');
63+
nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule;
64+
}
65+
66+
return nodeFetchModule.Response;
67+
}
68+
69+
// Export types as any for CJS compatibility
70+
export type RequestInit = any;
71+
export type HeadersInit = any;
72+
export type Request = any;
73+
export type Response = any;

src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
// This file is generated by scripts/exportVersion.js
2-
export const SDK_VERSION = '17.13.1';
2+
export const SDK_VERSION = '17.13.1-canary.4';

tests/apiClient.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ describe('APIClient', () => {
162162
});
163163

164164
describe('newRequest', () => {
165-
it('should set all the fields properly', () => {
165+
it('should set all the fields properly', async () => {
166166
client.headers = {
167167
'global-header': 'global-value',
168168
};
@@ -178,7 +178,7 @@ describe('APIClient', () => {
178178
headers: { override: 'bar' },
179179
},
180180
};
181-
const newRequest = client.newRequest(options);
181+
const newRequest = await client.newRequest(options);
182182

183183
expect(newRequest.method).toBe('POST');
184184
expect(newRequest.headers.raw()).toEqual({

tsconfig.cjs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
"allowSyntheticDefaultImports": true,
88
"moduleResolution": "node"
99
},
10+
"exclude": ["src/utils/fetchWrapper-esm.ts"]
1011
}

tsconfig.esm.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"extends": "./tsconfig.json",
33
"compilerOptions": {
44
"module": "es2020",
5-
"outDir": "lib/esm",
5+
"outDir": "lib/esm"
66
},
7+
"exclude": ["src/utils/fetchWrapper-cjs.ts"]
78
}

0 commit comments

Comments
 (0)