From 80b526fdf5eb3b3a90d188f3eed5ee05637ae955 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Wed, 17 Sep 2025 10:29:48 -0400 Subject: [PATCH 1/7] Release v7.13.1 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2332692..9521a1ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [7.13.1] - 2025-09-17 ### Fixed - Broken CJS build outputs resulted in a "TypeError: Nylas is not a constructor" error From 4b046860245166fbb2dc18548d0a9144653aafbb Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Wed, 17 Sep 2025 10:29:57 -0400 Subject: [PATCH 2/7] 17.13.1 --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1832d04d..979e7216 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nylas", - "version": "7.13.0", + "version": "17.13.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nylas", - "version": "7.13.0", + "version": "17.13.1", "license": "MIT", "dependencies": { "change-case": "^4.1.2", diff --git a/package.json b/package.json index d2625507..3cf1d1da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nylas", - "version": "7.13.0", + "version": "17.13.1", "description": "A NodeJS wrapper for the Nylas REST API for email, contacts, and calendar.", "main": "lib/cjs/nylas.js", "types": "lib/types/nylas.d.ts", diff --git a/src/version.ts b/src/version.ts index 9719a499..6103504a 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ // This file is generated by scripts/exportVersion.js -export const SDK_VERSION = '7.13.0'; +export const SDK_VERSION = '17.13.1'; From 0795581ad60090bc1761b8c67033feb9becdb724 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Thu, 18 Sep 2025 09:27:50 -0400 Subject: [PATCH 3/7] Added missing cjs-wrapper to the published builds --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cf1d1da..7f24a2d0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "types": "lib/types/nylas.d.ts", "module": "lib/esm/nylas.js", "files": [ - "lib" + "lib", + "cjs-wrapper.js", + "cjs-wrapper.d.ts" ], "engines": { "node": ">=16" From 95789356a0df6da3ba171eafca3ad5ae5834678c Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Thu, 18 Sep 2025 09:32:37 -0400 Subject: [PATCH 4/7] Updated changelog date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9521a1ae..ea2019af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [7.13.1] - 2025-09-17 +## [7.13.1] - 2025-09-18 ### Fixed - Broken CJS build outputs resulted in a "TypeError: Nylas is not a constructor" error From 4902d86bc46b561e2b93a7f4ff22486881d68c3a Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Mon, 22 Sep 2025 12:55:51 -0400 Subject: [PATCH 5/7] 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. --- package.json | 4 +- scripts/setupFetchWrapper.js | 29 ++++++++++++++ src/apiClient.ts | 11 ++++-- src/utils/fetchWrapper-cjs.ts | 73 +++++++++++++++++++++++++++++++++++ src/utils/fetchWrapper-esm.ts | 30 ++++++++++++++ src/utils/fetchWrapper.ts | 73 +++++++++++++++++++++++++++++++++++ src/version.ts | 2 +- tests/apiClient.spec.ts | 4 +- tsconfig.cjs.json | 1 + tsconfig.esm.json | 3 +- tsconfig.json | 3 +- 11 files changed, 222 insertions(+), 11 deletions(-) create mode 100644 scripts/setupFetchWrapper.js create mode 100644 src/utils/fetchWrapper-cjs.ts create mode 100644 src/utils/fetchWrapper-esm.ts create mode 100644 src/utils/fetchWrapper.ts diff --git a/package.json b/package.json index 7f24a2d0..fb2cbfb9 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "generate-model-index": "node scripts/generateModelIndex.js", "prebuild": "npm run export-version && npm run generate-model-index", "build": "rm -rf lib && npm run build-esm && npm run build-cjs && npm run generate-lib-package-json", - "build-esm": "tsc -p tsconfig.esm.json", - "build-cjs": "tsc -p tsconfig.cjs.json", + "build-esm": "node scripts/setupFetchWrapper.js esm && tsc -p tsconfig.esm.json", + "build-cjs": "node scripts/setupFetchWrapper.js cjs && tsc -p tsconfig.cjs.json", "prepare": "npm run build", "build:docs": "typedoc --out docs", "version": "npm run export-version && git add src/version.ts" diff --git a/scripts/setupFetchWrapper.js b/scripts/setupFetchWrapper.js new file mode 100644 index 00000000..3d478857 --- /dev/null +++ b/scripts/setupFetchWrapper.js @@ -0,0 +1,29 @@ +/** + * Script to copy the appropriate fetchWrapper implementation based on build type + */ + +const fs = require('fs'); +const path = require('path'); + +const buildType = process.argv[2]; // 'esm' or 'cjs' + +if (!buildType || !['esm', 'cjs'].includes(buildType)) { + console.error('Usage: node setupFetchWrapper.js '); + process.exit(1); +} + +const srcDir = path.join(__dirname, '..', 'src', 'utils'); +const sourceFile = path.join(srcDir, `fetchWrapper-${buildType}.ts`); +const targetFile = path.join(srcDir, 'fetchWrapper.ts'); + +// Ensure source file exists +if (!fs.existsSync(sourceFile)) { + console.error(`Source file ${sourceFile} does not exist`); + process.exit(1); +} + +// Copy the appropriate implementation +fs.copyFileSync(sourceFile, targetFile); +/* eslint-disable no-console */ +console.log(`✅ Copied fetchWrapper-${buildType}.ts to fetchWrapper.ts`); +/* eslint-enable no-console */ diff --git a/src/apiClient.ts b/src/apiClient.ts index ddc85f40..010d525e 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -1,4 +1,3 @@ -import fetch, { Request, Response } from 'node-fetch'; import { Readable as _Readable } from 'node:stream'; import { NylasConfig, OverridableNylasConfig } from './config.js'; import { @@ -11,6 +10,8 @@ import { SDK_VERSION } from './version.js'; import { FormData } from 'formdata-node'; import { FormDataEncoder as _FormDataEncoder } from 'form-data-encoder'; import { snakeCase } from 'change-case'; +import { getFetch, getRequest } from './utils/fetchWrapper.js'; +import type { Request, Response } from './utils/fetchWrapper.js'; /** * The header key for the debugging flow ID @@ -155,7 +156,7 @@ export default class APIClient { } private async sendRequest(options: RequestOptionsParams): Promise { - const req = this.newRequest(options); + const req = await this.newRequest(options); const controller: AbortController = new AbortController(); // Handle timeout @@ -177,6 +178,7 @@ export default class APIClient { }, timeoutDuration); try { + const fetch = await getFetch(); const response = await fetch(req, { signal: controller.signal as AbortSignal, }); @@ -271,9 +273,10 @@ export default class APIClient { return requestOptions; } - newRequest(options: RequestOptionsParams): Request { + async newRequest(options: RequestOptionsParams): Promise { const newOptions = this.requestOptions(options); - return new Request(newOptions.url, { + const RequestConstructor = await getRequest(); + return new RequestConstructor(newOptions.url, { method: newOptions.method, headers: newOptions.headers, body: newOptions.body, diff --git a/src/utils/fetchWrapper-cjs.ts b/src/utils/fetchWrapper-cjs.ts new file mode 100644 index 00000000..0b873807 --- /dev/null +++ b/src/utils/fetchWrapper-cjs.ts @@ -0,0 +1,73 @@ +/** + * Fetch wrapper for CJS builds - uses dynamic imports for node-fetch compatibility + */ + +// Types for the dynamic import result +interface NodeFetchModule { + default: any; // fetch function + Request: any; // Request constructor + Response: any; // Response constructor +} + +// Cache for the dynamically imported module +let nodeFetchModule: NodeFetchModule | null = null; + +/** + * Get fetch function - uses dynamic import for CJS + */ +export async function getFetch(): Promise { + // In test environment, use global fetch (mocked by jest-fetch-mock) + if (typeof global !== 'undefined' && global.fetch) { + return global.fetch; + } + + if (!nodeFetchModule) { + // Use Function constructor to prevent TypeScript from converting to require() + const dynamicImport = new Function('specifier', 'return import(specifier)'); + nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule; + } + + return nodeFetchModule.default; +} + +/** + * Get Request constructor - uses dynamic import for CJS + */ +export async function getRequest(): Promise { + // In test environment, use global Request or a mock + if (typeof global !== 'undefined' && global.Request) { + return global.Request; + } + + if (!nodeFetchModule) { + // Use Function constructor to prevent TypeScript from converting to require() + const dynamicImport = new Function('specifier', 'return import(specifier)'); + nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule; + } + + return nodeFetchModule.Request; +} + +/** + * Get Response constructor - uses dynamic import for CJS + */ +export async function getResponse(): Promise { + // In test environment, use global Response or a mock + if (typeof global !== 'undefined' && global.Response) { + return global.Response; + } + + if (!nodeFetchModule) { + // Use Function constructor to prevent TypeScript from converting to require() + const dynamicImport = new Function('specifier', 'return import(specifier)'); + nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule; + } + + return nodeFetchModule.Response; +} + +// Export types as any for CJS compatibility +export type RequestInit = any; +export type HeadersInit = any; +export type Request = any; +export type Response = any; diff --git a/src/utils/fetchWrapper-esm.ts b/src/utils/fetchWrapper-esm.ts new file mode 100644 index 00000000..91395b32 --- /dev/null +++ b/src/utils/fetchWrapper-esm.ts @@ -0,0 +1,30 @@ +/** + * Fetch wrapper for ESM builds - uses static imports for optimal performance + */ + +import fetch, { Request, Response } from 'node-fetch'; +import type { RequestInit, HeadersInit } from 'node-fetch'; + +/** + * Get fetch function - uses static import for ESM + */ +export async function getFetch(): Promise { + return fetch; +} + +/** + * Get Request constructor - uses static import for ESM + */ +export async function getRequest(): Promise { + return Request; +} + +/** + * Get Response constructor - uses static import for ESM + */ +export async function getResponse(): Promise { + return Response; +} + +// Export types directly +export type { RequestInit, HeadersInit, Request, Response }; diff --git a/src/utils/fetchWrapper.ts b/src/utils/fetchWrapper.ts new file mode 100644 index 00000000..0b873807 --- /dev/null +++ b/src/utils/fetchWrapper.ts @@ -0,0 +1,73 @@ +/** + * Fetch wrapper for CJS builds - uses dynamic imports for node-fetch compatibility + */ + +// Types for the dynamic import result +interface NodeFetchModule { + default: any; // fetch function + Request: any; // Request constructor + Response: any; // Response constructor +} + +// Cache for the dynamically imported module +let nodeFetchModule: NodeFetchModule | null = null; + +/** + * Get fetch function - uses dynamic import for CJS + */ +export async function getFetch(): Promise { + // In test environment, use global fetch (mocked by jest-fetch-mock) + if (typeof global !== 'undefined' && global.fetch) { + return global.fetch; + } + + if (!nodeFetchModule) { + // Use Function constructor to prevent TypeScript from converting to require() + const dynamicImport = new Function('specifier', 'return import(specifier)'); + nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule; + } + + return nodeFetchModule.default; +} + +/** + * Get Request constructor - uses dynamic import for CJS + */ +export async function getRequest(): Promise { + // In test environment, use global Request or a mock + if (typeof global !== 'undefined' && global.Request) { + return global.Request; + } + + if (!nodeFetchModule) { + // Use Function constructor to prevent TypeScript from converting to require() + const dynamicImport = new Function('specifier', 'return import(specifier)'); + nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule; + } + + return nodeFetchModule.Request; +} + +/** + * Get Response constructor - uses dynamic import for CJS + */ +export async function getResponse(): Promise { + // In test environment, use global Response or a mock + if (typeof global !== 'undefined' && global.Response) { + return global.Response; + } + + if (!nodeFetchModule) { + // Use Function constructor to prevent TypeScript from converting to require() + const dynamicImport = new Function('specifier', 'return import(specifier)'); + nodeFetchModule = (await dynamicImport('node-fetch')) as NodeFetchModule; + } + + return nodeFetchModule.Response; +} + +// Export types as any for CJS compatibility +export type RequestInit = any; +export type HeadersInit = any; +export type Request = any; +export type Response = any; diff --git a/src/version.ts b/src/version.ts index 6103504a..d124b337 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ // This file is generated by scripts/exportVersion.js -export const SDK_VERSION = '17.13.1'; +export const SDK_VERSION = '17.13.1-canary.4'; diff --git a/tests/apiClient.spec.ts b/tests/apiClient.spec.ts index ed379ad3..ccedefc4 100644 --- a/tests/apiClient.spec.ts +++ b/tests/apiClient.spec.ts @@ -162,7 +162,7 @@ describe('APIClient', () => { }); describe('newRequest', () => { - it('should set all the fields properly', () => { + it('should set all the fields properly', async () => { client.headers = { 'global-header': 'global-value', }; @@ -178,7 +178,7 @@ describe('APIClient', () => { headers: { override: 'bar' }, }, }; - const newRequest = client.newRequest(options); + const newRequest = await client.newRequest(options); expect(newRequest.method).toBe('POST'); expect(newRequest.headers.raw()).toEqual({ diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index 2ee5ebc7..4b2a2637 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -7,4 +7,5 @@ "allowSyntheticDefaultImports": true, "moduleResolution": "node" }, + "exclude": ["src/utils/fetchWrapper-esm.ts"] } diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 5ec6cd60..b66b56f4 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "module": "es2020", - "outDir": "lib/esm", + "outDir": "lib/esm" }, + "exclude": ["src/utils/fetchWrapper-cjs.ts"] } diff --git a/tsconfig.json b/tsconfig.json index d1799ecb..f09b6124 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "moduleResolution": "node", "strict": true, "noImplicitAny": true, - "noImplicitThis": true + "noImplicitThis": true, + "baseUrl": "./src" }, "typedocOptions": { "entryPointStrategy": "expand", From 2c6342ca4fb93e1835ecc21a1bc77e214daf9208 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Mon, 22 Sep 2025 16:30:08 -0400 Subject: [PATCH 6/7] Added tests --- src/version.ts | 2 +- tests/utils/fetchWrapper-cjs.spec.ts | 203 +++++++++++++++++++++++++++ tests/utils/fetchWrapper-esm.spec.ts | 187 ++++++++++++++++++++++++ tests/utils/fetchWrapper.spec.ts | 202 ++++++++++++++++++++++++++ 4 files changed, 593 insertions(+), 1 deletion(-) create mode 100644 tests/utils/fetchWrapper-cjs.spec.ts create mode 100644 tests/utils/fetchWrapper-esm.spec.ts create mode 100644 tests/utils/fetchWrapper.spec.ts diff --git a/src/version.ts b/src/version.ts index d124b337..bfc3a35b 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ // This file is generated by scripts/exportVersion.js -export const SDK_VERSION = '17.13.1-canary.4'; +export const SDK_VERSION = '7.13.1'; diff --git a/tests/utils/fetchWrapper-cjs.spec.ts b/tests/utils/fetchWrapper-cjs.spec.ts new file mode 100644 index 00000000..126fe18a --- /dev/null +++ b/tests/utils/fetchWrapper-cjs.spec.ts @@ -0,0 +1,203 @@ +/** + * Tests for CJS fetchWrapper implementation + */ + +// Import types are only used for dynamic imports in tests, so we don't import them here +// The functions are imported dynamically within each test to ensure proper module isolation + +// Mock the dynamic import to avoid actually importing node-fetch +const mockNodeFetch = { + default: jest.fn().mockName('mockFetch'), + Request: jest.fn().mockName('mockRequest'), + Response: jest.fn().mockName('mockResponse'), +}; + +// Mock the Function constructor used for dynamic imports +const mockDynamicImport = jest.fn().mockResolvedValue(mockNodeFetch); +global.Function = jest.fn().mockImplementation(() => mockDynamicImport); + +// Mock global objects for test environment detection +const originalGlobal = global; + +describe('fetchWrapper-cjs', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Reset the module cache by clearing the nodeFetchModule cache + // This is done by reimporting the module + jest.resetModules(); + // Setup mocked Function constructor + global.Function = jest.fn().mockImplementation(() => mockDynamicImport); + // Reset mock implementation + mockDynamicImport.mockResolvedValue(mockNodeFetch); + }); + + describe('getFetch', () => { + it('should return global.fetch when in test environment', async () => { + const mockGlobalFetch = jest.fn().mockName('globalFetch'); + (global as any).fetch = mockGlobalFetch; + + const { getFetch } = await import('../../src/utils/fetchWrapper-cjs.js'); + const fetch = await getFetch(); + + expect(fetch).toBe(mockGlobalFetch); + }); + + it('should use dynamic import when global.fetch is not available', async () => { + // Remove global.fetch + delete (global as any).fetch; + + const { getFetch } = await import('../../src/utils/fetchWrapper-cjs.js'); + const fetch = await getFetch(); + + expect(fetch).toBe(mockNodeFetch.default); + }); + + it('should return a function', async () => { + delete (global as any).fetch; + + const { getFetch } = await import('../../src/utils/fetchWrapper-cjs.js'); + const fetch = await getFetch(); + + expect(typeof fetch).toBe('function'); + }); + + it('should handle Function constructor usage', async () => { + delete (global as any).fetch; + + const { getFetch } = await import('../../src/utils/fetchWrapper-cjs.js'); + await getFetch(); + + // Verify Function constructor was called + expect(global.Function).toHaveBeenCalled(); + }); + }); + + describe('getRequest', () => { + it('should return global.Request when in test environment', async () => { + const mockGlobalRequest = jest.fn().mockName('globalRequest'); + (global as any).Request = mockGlobalRequest; + + const { getRequest } = await import( + '../../src/utils/fetchWrapper-cjs.js' + ); + const Request = await getRequest(); + + expect(Request).toBe(mockGlobalRequest); + }); + + it('should use dynamic import when global.Request is not available', async () => { + delete (global as any).Request; + + const { getRequest } = await import( + '../../src/utils/fetchWrapper-cjs.js' + ); + const Request = await getRequest(); + + expect(Request).toBe(mockNodeFetch.Request); + }); + + it('should return a function', async () => { + delete (global as any).Request; + + const { getRequest } = await import( + '../../src/utils/fetchWrapper-cjs.js' + ); + const Request = await getRequest(); + + expect(typeof Request).toBe('function'); + }); + }); + + describe('getResponse', () => { + it('should return global.Response when in test environment', async () => { + const mockGlobalResponse = jest.fn().mockName('globalResponse'); + (global as any).Response = mockGlobalResponse; + + const { getResponse } = await import( + '../../src/utils/fetchWrapper-cjs.js' + ); + const Response = await getResponse(); + + expect(Response).toBe(mockGlobalResponse); + }); + + it('should use dynamic import when global.Response is not available', async () => { + delete (global as any).Response; + + const { getResponse } = await import( + '../../src/utils/fetchWrapper-cjs.js' + ); + const Response = await getResponse(); + + expect(Response).toBe(mockNodeFetch.Response); + }); + + it('should return a function', async () => { + delete (global as any).Response; + + const { getResponse } = await import( + '../../src/utils/fetchWrapper-cjs.js' + ); + const Response = await getResponse(); + + expect(typeof Response).toBe('function'); + }); + }); + + describe('mixed environment scenarios', () => { + it('should prefer global objects when available, fall back to dynamic import for missing ones', async () => { + const mockGlobalFetch = jest.fn().mockName('globalFetch'); + (global as any).fetch = mockGlobalFetch; + delete (global as any).Request; + delete (global as any).Response; + + const { getFetch, getRequest, getResponse } = await import( + '../../src/utils/fetchWrapper-cjs.js' + ); + + const fetch = await getFetch(); + const Request = await getRequest(); + const Response = await getResponse(); + + // fetch should use global + expect(fetch).toBe(mockGlobalFetch); + + // Request and Response should use dynamic import + expect(Request).toBe(mockNodeFetch.Request); + expect(Response).toBe(mockNodeFetch.Response); + }); + + it('should handle undefined global object gracefully', async () => { + // Simulate environment where global might be undefined + const originalGlobal = global; + (global as any) = undefined; + + const { getFetch } = await import('../../src/utils/fetchWrapper-cjs.js'); + const fetch = await getFetch(); + + expect(fetch).toBe(mockNodeFetch.default); + + // Restore global + (global as any) = originalGlobal; + }); + }); + + describe('Type exports', () => { + it('should export types as any for CJS compatibility', () => { + // This test ensures that the types are properly exported as 'any' + // The actual type checking happens at compile time + expect(true).toBe(true); + }); + }); + + afterEach(() => { + // Clean up global modifications + delete (global as any).fetch; + delete (global as any).Request; + delete (global as any).Response; + // Restore original Function constructor if needed + if (originalGlobal.Function) { + global.Function = originalGlobal.Function; + } + }); +}); diff --git a/tests/utils/fetchWrapper-esm.spec.ts b/tests/utils/fetchWrapper-esm.spec.ts new file mode 100644 index 00000000..a49b408b --- /dev/null +++ b/tests/utils/fetchWrapper-esm.spec.ts @@ -0,0 +1,187 @@ +/** + * Tests for ESM fetchWrapper implementation + */ + +import { + getFetch, + getRequest, + getResponse, +} from '../../src/utils/fetchWrapper-esm.js'; + +describe('fetchWrapper-esm', () => { + describe('getFetch', () => { + it('should return the node-fetch function', async () => { + const fetch = await getFetch(); + expect(typeof fetch).toBe('function'); + // The actual function name might vary, just check it's a function + expect(fetch).toBeDefined(); + }); + + it('should return the same fetch function on multiple calls', async () => { + const fetch1 = await getFetch(); + const fetch2 = await getFetch(); + expect(fetch1).toBe(fetch2); + }); + + it('should be able to make a basic fetch request', async () => { + const fetch = await getFetch(); + + // We can't actually make HTTP requests in tests, but we can verify + // the fetch function has the expected interface + expect(fetch).toBeDefined(); + expect(typeof fetch).toBe('function'); + }); + }); + + describe('getRequest', () => { + it('should return the Request constructor', async () => { + const Request = await getRequest(); + expect(typeof Request).toBe('function'); + // The actual function name might vary, just check it's a constructor + expect(Request).toBeDefined(); + }); + + it('should return the same Request constructor on multiple calls', async () => { + const Request1 = await getRequest(); + const Request2 = await getRequest(); + expect(Request1).toBe(Request2); + }); + + it('should be able to create a Request instance', async () => { + const RequestConstructor = await getRequest(); + const request = new RequestConstructor('https://example.com', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ test: 'data' }), + }); + + expect(request).toBeInstanceOf(RequestConstructor); + expect(request.url).toBe('https://example.com/'); + expect(request.method).toBe('POST'); + expect(request.headers.get('content-type')).toBe('application/json'); + }); + + it('should create Request with different HTTP methods', async () => { + const RequestConstructor = await getRequest(); + + const getReq = new RequestConstructor('https://example.com', { + method: 'GET', + }); + const postReq = new RequestConstructor('https://example.com', { + method: 'POST', + }); + const putReq = new RequestConstructor('https://example.com', { + method: 'PUT', + }); + const deleteReq = new RequestConstructor('https://example.com', { + method: 'DELETE', + }); + + expect(getReq.method).toBe('GET'); + expect(postReq.method).toBe('POST'); + expect(putReq.method).toBe('PUT'); + expect(deleteReq.method).toBe('DELETE'); + }); + + it('should handle Request with custom headers', async () => { + const RequestConstructor = await getRequest(); + const request = new RequestConstructor('https://example.com', { + headers: { + Authorization: 'Bearer token', + 'X-Custom-Header': 'custom-value', + }, + }); + + expect(request.headers.get('authorization')).toBe('Bearer token'); + expect(request.headers.get('x-custom-header')).toBe('custom-value'); + }); + }); + + describe('getResponse', () => { + it('should return the Response constructor', async () => { + const Response = await getResponse(); + expect(typeof Response).toBe('function'); + // The actual function name might vary, just check it's a constructor + expect(Response).toBeDefined(); + }); + + it('should return the same Response constructor on multiple calls', async () => { + const Response1 = await getResponse(); + const Response2 = await getResponse(); + expect(Response1).toBe(Response2); + }); + + it('should be able to create a Response instance', async () => { + const ResponseConstructor = await getResponse(); + const response = new ResponseConstructor('{"test": "data"}', { + status: 200, + statusText: 'OK', + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Check that response has the expected properties instead of instanceof check + expect(response.status).toBeDefined(); + expect(response.headers).toBeDefined(); + expect(response.status).toBe(200); + expect(response.statusText).toBe('OK'); + expect(response.headers.get('content-type')).toBe('application/json'); + + const json = await response.json(); + expect(json).toEqual({ test: 'data' }); + }); + + it('should create Response with different status codes', async () => { + const ResponseConstructor = await getResponse(); + + const okResponse = new ResponseConstructor('OK', { status: 200 }); + const notFoundResponse = new ResponseConstructor('Not Found', { + status: 404, + }); + const serverErrorResponse = new ResponseConstructor('Server Error', { + status: 500, + }); + + expect(okResponse.status).toBe(200); + expect(okResponse.ok).toBe(true); + + expect(notFoundResponse.status).toBe(404); + expect(notFoundResponse.ok).toBe(false); + + expect(serverErrorResponse.status).toBe(500); + expect(serverErrorResponse.ok).toBe(false); + }); + + it('should handle Response with different content types', async () => { + const ResponseConstructor = await getResponse(); + + const jsonResponse = new ResponseConstructor('{"key": "value"}', { + headers: { 'Content-Type': 'application/json' }, + }); + + const textResponse = new ResponseConstructor('plain text', { + headers: { 'Content-Type': 'text/plain' }, + }); + + expect(jsonResponse.headers.get('content-type')).toBe('application/json'); + expect(textResponse.headers.get('content-type')).toBe('text/plain'); + + const jsonData = await jsonResponse.json(); + expect(jsonData).toEqual({ key: 'value' }); + + const textData = await textResponse.text(); + expect(textData).toBe('plain text'); + }); + }); + + describe('Type exports', () => { + it('should have proper TypeScript types available', () => { + // This test ensures that the types are properly exported + // The actual type checking happens at compile time + expect(true).toBe(true); + }); + }); +}); diff --git a/tests/utils/fetchWrapper.spec.ts b/tests/utils/fetchWrapper.spec.ts new file mode 100644 index 00000000..008ed1e3 --- /dev/null +++ b/tests/utils/fetchWrapper.spec.ts @@ -0,0 +1,202 @@ +/** + * Tests for main fetchWrapper implementation (CJS-based) + */ + +// Store original global functions +const _originalGlobal = global; +const originalFunction = global.Function; + +// Mock the dynamic import to avoid actually importing node-fetch +const mockNodeFetchMain = { + default: jest.fn().mockName('mockFetch'), + Request: jest.fn().mockName('mockRequest'), + Response: jest.fn().mockName('mockResponse'), +}; + +// Mock the Function constructor used for dynamic imports +const mockDynamicImportMain = jest.fn().mockResolvedValue(mockNodeFetchMain); + +describe('fetchWrapper (main)', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + // Setup mocked Function constructor + global.Function = jest.fn().mockImplementation(() => mockDynamicImportMain); + }); + + describe('integration with apiClient usage patterns', () => { + it('should work with typical apiClient.request() usage', async () => { + const mockGlobalFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ data: 'test' }), + headers: new Map([['content-type', 'application/json']]), + }); + (global as any).fetch = mockGlobalFetch; + + const { getFetch } = await import('../../src/utils/fetchWrapper.js'); + const fetch = await getFetch(); + const response = await fetch('https://api.nylas.com/v3/grants', { + method: 'GET', + headers: { + Authorization: 'Bearer token', + 'Content-Type': 'application/json', + }, + }); + + expect(mockGlobalFetch).toHaveBeenCalledWith( + 'https://api.nylas.com/v3/grants', + { + method: 'GET', + headers: { + Authorization: 'Bearer token', + 'Content-Type': 'application/json', + }, + } + ); + expect(response.ok).toBe(true); + expect(response.status).toBe(200); + }); + + it('should work with apiClient.newRequest() usage pattern', async () => { + const mockGlobalRequest = jest + .fn() + .mockImplementation((url, options) => ({ + url, + method: options?.method || 'GET', + headers: new Map(Object.entries(options?.headers || {})), + body: options?.body, + })); + (global as any).Request = mockGlobalRequest; + + const { getRequest } = await import('../../src/utils/fetchWrapper.js'); + const Request = await getRequest(); + const request = new Request('https://api.nylas.com/v3/grants/123', { + method: 'PUT', + headers: { + Authorization: 'Bearer token', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: 'Updated Grant' }), + }); + + expect(mockGlobalRequest).toHaveBeenCalledWith( + 'https://api.nylas.com/v3/grants/123', + { + method: 'PUT', + headers: { + Authorization: 'Bearer token', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: 'Updated Grant' }), + } + ); + expect(request.method).toBe('PUT'); + expect(request.url).toBe('https://api.nylas.com/v3/grants/123'); + }); + }); + + describe('consistency with CJS implementation', () => { + it('should behave identically to fetchWrapper-cjs', async () => { + const mockGlobalFetch = jest.fn().mockName('globalFetch'); + (global as any).fetch = mockGlobalFetch; + + const { getFetch } = await import('../../src/utils/fetchWrapper.js'); + const fetch = await getFetch(); + expect(fetch).toBe(mockGlobalFetch); + }); + + it('should use dynamic imports when globals are not available', async () => { + delete (global as any).fetch; + delete (global as any).Request; + delete (global as any).Response; + + const { getFetch, getRequest, getResponse } = await import( + '../../src/utils/fetchWrapper.js' + ); + const fetch = await getFetch(); + const Request = await getRequest(); + const Response = await getResponse(); + + expect(mockDynamicImportMain).toHaveBeenCalledWith('node-fetch'); + expect(fetch).toBe(mockNodeFetchMain.default); + expect(Request).toBe(mockNodeFetchMain.Request); + expect(Response).toBe(mockNodeFetchMain.Response); + }); + }); + + describe('basic functionality', () => { + it('should return functions for all methods', async () => { + delete (global as any).fetch; + delete (global as any).Request; + delete (global as any).Response; + + const { getFetch, getRequest, getResponse } = await import( + '../../src/utils/fetchWrapper.js' + ); + + const fetch = await getFetch(); + const Request = await getRequest(); + const Response = await getResponse(); + + expect(typeof fetch).toBe('function'); + expect(typeof Request).toBe('function'); + expect(typeof Response).toBe('function'); + }); + + it('should prefer global objects when available', async () => { + const mockGlobalFetch = jest.fn(); + const mockGlobalRequest = jest.fn(); + const mockGlobalResponse = jest.fn(); + + (global as any).fetch = mockGlobalFetch; + (global as any).Request = mockGlobalRequest; + (global as any).Response = mockGlobalResponse; + + const { getFetch, getRequest, getResponse } = await import( + '../../src/utils/fetchWrapper.js' + ); + const fetch = await getFetch(); + const Request = await getRequest(); + const Response = await getResponse(); + + expect(fetch).toBe(mockGlobalFetch); + expect(Request).toBe(mockGlobalRequest); + expect(Response).toBe(mockGlobalResponse); + }); + }); + + describe('environment detection', () => { + it('should detect test environment correctly', async () => { + const mockGlobalFetch = jest.fn(); + (global as any).fetch = mockGlobalFetch; + + const { getFetch } = await import('../../src/utils/fetchWrapper.js'); + const fetch = await getFetch(); + expect(fetch).toBe(mockGlobalFetch); + }); + + it('should handle missing global object', async () => { + // Simulate environment where global is not defined + const _localOriginalGlobal = global; + (global as any) = undefined; + + const { getFetch } = await import('../../src/utils/fetchWrapper.js'); + const fetch = await getFetch(); + expect(mockDynamicImportMain).toHaveBeenCalledWith('node-fetch'); + expect(fetch).toBe(mockNodeFetchMain.default); + + // Restore global + (global as any) = _localOriginalGlobal; + }); + }); + + afterEach(() => { + // Clean up global modifications + delete (global as any).fetch; + delete (global as any).Request; + delete (global as any).Response; + // Restore original Function constructor + global.Function = originalFunction; + }); +}); From 7eef782162268a0f69f662ca86bd6362af3301ac Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Tue, 23 Sep 2025 10:50:52 -0400 Subject: [PATCH 7/7] Fixed package.json version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb2cbfb9..711933b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nylas", - "version": "17.13.1", + "version": "7.13.1", "description": "A NodeJS wrapper for the Nylas REST API for email, contacts, and calendar.", "main": "lib/cjs/nylas.js", "types": "lib/types/nylas.d.ts",