From fee6d08a94a555af13717998eb354441e22a5b10 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 4 Aug 2026 20:01:25 +0530 Subject: [PATCH 1/4] fix: Resolve require() incompatibility in ESM and UMD builds Three environments broke when keep-alive agents were introduced using require('http') and require('https'): 1. Native ESM (ReferenceError: require is not defined) - Added typeof require === 'undefined' guard in createKeepAliveAgent() - Falls back to false (pre-1.5.0 behaviour) in ESM environments 2. Turbopack/webpack (expression is too dynamic) - Replaced require(moduleName) variable arg with static string literals - require('http') / require('https') are statically analysable by bundlers 3. UMD webpack build (Can't resolve 'http') - Added externals for http and https in config/webpack.config.js - UMD wrapper resolves them at runtime in Node; isNodeEnvironment guard prevents execution in browser where they are not available Tests added: - test/esm-compat.mjs: native ESM integration test (run after build) - test/contentstack-core.node-agent.spec.ts: Jest wrapper spawning esm-compat.mjs - test/esm-exports.spec.ts: static analysis asserting no dynamic require(variable) Co-Authored-By: Claude Sonnet 4.6 --- .talismanrc | 2 ++ config/webpack.config.js | 8 +++++ package-lock.json | 4 +-- package.json | 2 +- src/lib/contentstack-core.ts | 8 +++-- test/contentstack-core.node-agent.spec.ts | 16 +++++++++ test/esm-compat.mjs | 44 +++++++++++++++++++++++ test/esm-exports.spec.ts | 24 +++++++++++++ 8 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 test/esm-compat.mjs diff --git a/.talismanrc b/.talismanrc index d43080e..ef237b6 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,4 +1,6 @@ fileignoreconfig: - filename: package-lock.json checksum: 1cba2ff4ea6e6f725f6d22bc8c2e04e46c30923986124ef7b4689c705a0397f1 +- filename: test/esm-compat.mjs + checksum: 6da8aca15e54c00fe69b0226153b5d641e6f79845cd63adf5d38059932c15ae8 version: "" \ No newline at end of file diff --git a/config/webpack.config.js b/config/webpack.config.js index 0834619..91d8c69 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -27,4 +27,12 @@ module.exports = { resolve: { extensions: ['.ts', '.js', '.tsx', '.jsx'], }, + // 'http' and 'https' are Node-only built-ins. Marking them external means: + // - In Node.js (CJS/UMD): the UMD wrapper calls require('http') at runtime → real module + // - In browser: UMD wrapper looks up window['_'] → undefined, but isNodeEnvironment + // is false so createKeepAliveAgent() short-circuits before reaching require() + externals: { + http: { commonjs: 'http', commonjs2: 'http', root: '_' }, + https: { commonjs: 'https', commonjs2: 'https', root: '_' }, + }, }; diff --git a/package-lock.json b/package-lock.json index 3d441c0..0a92026 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/core", - "version": "1.5.0", + "version": "1.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/core", - "version": "1.5.0", + "version": "1.5.1", "license": "MIT", "dependencies": { "axios": "^1.18.1", diff --git a/package.json b/package.json index e4d282b..7a03409 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/core", - "version": "1.5.0", + "version": "1.5.1", "type": "commonjs", "main": "./dist/cjs/src/index.js", "types": "./dist/cjs/src/index.d.ts", diff --git a/src/lib/contentstack-core.ts b/src/lib/contentstack-core.ts index 8a2aa2d..ffcdabf 100644 --- a/src/lib/contentstack-core.ts +++ b/src/lib/contentstack-core.ts @@ -6,13 +6,15 @@ import { ERROR_MESSAGES } from './error-messages'; const isNodeEnvironment = typeof window === 'undefined'; -// Guarded require: keeps 'http'/'https' out of browser bundles, which have no browser field of their own to redirect this. +// Guarded require: keeps 'http'/'https' out of browser bundles and ESM environments. +// Uses string literals (not the variable) so webpack/Turbopack can statically analyze the require call. function createKeepAliveAgent(moduleName: 'http' | 'https') { - if (!isNodeEnvironment) { + if (!isNodeEnvironment || typeof require === 'undefined') { return false as const; } - return new (require(moduleName).Agent)({ keepAlive: true }); + if (moduleName === 'http') return new (require('http').Agent)({ keepAlive: true }); + return new (require('https').Agent)({ keepAlive: true }); } export function httpClient(options: HttpClientParams): AxiosInstance { diff --git a/test/contentstack-core.node-agent.spec.ts b/test/contentstack-core.node-agent.spec.ts index 508345b..0228687 100644 --- a/test/contentstack-core.node-agent.spec.ts +++ b/test/contentstack-core.node-agent.spec.ts @@ -3,6 +3,8 @@ */ import http from 'http'; import https from 'https'; +import { execFile } from 'child_process'; +import * as path from 'path'; import { httpClient } from '../src/lib/contentstack-core'; describe('httpClient default connection agents (Node environment)', () => { @@ -19,4 +21,18 @@ describe('httpClient default connection agents (Node environment)', () => { expect(instance.defaults.httpsAgent).toBeInstanceOf(https.Agent); expect((instance.defaults.httpsAgent as any).keepAlive).toBe(true); }); + + it('should not throw and fall back to false agents in native ESM (require is not defined)', (done) => { + // Runs test/esm-compat.mjs as a native ESM child process. + // Regression test for contentstack-js-core#246: ReferenceError: require is not defined. + // Requires `npm run build` to have been run first. + const script = path.join(__dirname, 'esm-compat.mjs'); + execFile(process.execPath, [script], (error, stdout, stderr) => { + if (error) { + done(new Error(`Native ESM test failed:\n${stderr || stdout}`)); + } else { + done(); + } + }); + }); }); diff --git a/test/esm-compat.mjs b/test/esm-compat.mjs new file mode 100644 index 0000000..d8b3aac --- /dev/null +++ b/test/esm-compat.mjs @@ -0,0 +1,44 @@ +/** + * Native ESM integration test for @contentstack/core. + * + * Run after `npm run build`: + * node test/esm-compat.mjs + * + * Verifies that importing httpClient via ESM (where require is not defined) + * does not throw a ReferenceError and falls back to false for http/https agents. + * This catches the regression reported in contentstack-js-core#246. + */ +import { httpClient } from '../dist/esm/src/index.js'; + +let failed = false; + +function assert(condition, message) { + if (!condition) { + process.stderr.write(`FAIL: ${message}\n`); + failed = true; + } else { + process.stdout.write(`PASS: ${message}\n`); + } +} + +let instance; +try { + instance = httpClient({}); +} catch (err) { + process.stderr.write(`FAIL: httpClient() threw in native ESM: ${err.message}\n`); + process.exit(1); +} + +// In native ESM, require is not defined, so createKeepAliveAgent must fall back to false +assert( + instance.defaults.httpAgent === false, + `httpAgent should be false in native ESM (got ${instance.defaults.httpAgent})` +); + +assert( + instance.defaults.httpsAgent === false, + `httpsAgent should be false in native ESM (got ${instance.defaults.httpsAgent})` +); + +if (failed) process.exit(1); +process.stdout.write('ESM compat: all checks passed\n'); diff --git a/test/esm-exports.spec.ts b/test/esm-exports.spec.ts index da07797..d266abb 100644 --- a/test/esm-exports.spec.ts +++ b/test/esm-exports.spec.ts @@ -106,6 +106,30 @@ describe('ESM Exports Tests', () => { }); }); + describe('ESM Build - require() Safety (Turbopack/webpack compatibility)', () => { + const esmCorePath = path.join(distPath, 'esm', 'src', 'lib', 'contentstack-core.js'); + + it('should have ESM contentstack-core.js built', () => { + expect(fs.existsSync(esmCorePath)).toBe(true); + }); + + it('should not contain dynamic require(variable) expressions that webpack/Turbopack reject', () => { + const content = fs.readFileSync(esmCorePath, 'utf-8'); + // Dynamic require(identifier) — bundlers cannot statically trace these and throw + // "Cannot find module as expression is too dynamic" + expect(content).not.toMatch(/require\([a-zA-Z_$]/); + }); + + it('should use only static string-literal require calls', () => { + const content = fs.readFileSync(esmCorePath, 'utf-8'); + const requireCalls = [...content.matchAll(/require\(([^)]+)\)/g)].map((m) => m[1].trim()); + for (const arg of requireCalls) { + // Every require() arg must be a quoted string literal + expect(arg).toMatch(/^['"`]/); + } + }); + }); + describe('Source Code Imports', () => { it('should be able to import getData as named export from source', async () => { const { getData } = await import('../src'); From 553b0e3223ef73382fc5e8a82aa82168a5c876d4 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 5 Aug 2026 11:56:38 +0530 Subject: [PATCH 2/4] fix: Enhance compatibility with ESM and UMD by refining HTTP agent creation logic --- package.json | 4 ++++ src/lib/contentstack-core.ts | 27 ++++++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 7a03409..4088889 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,10 @@ "type": "commonjs", "main": "./dist/cjs/src/index.js", "types": "./dist/cjs/src/index.d.ts", + "browser": { + "http": false, + "https": false + }, "exports": { ".": { "import": { diff --git a/src/lib/contentstack-core.ts b/src/lib/contentstack-core.ts index ffcdabf..dc29dcf 100644 --- a/src/lib/contentstack-core.ts +++ b/src/lib/contentstack-core.ts @@ -6,14 +6,23 @@ import { ERROR_MESSAGES } from './error-messages'; const isNodeEnvironment = typeof window === 'undefined'; -// Guarded require: keeps 'http'/'https' out of browser bundles and ESM environments. -// Uses string literals (not the variable) so webpack/Turbopack can statically analyze the require call. -function createKeepAliveAgent(moduleName: 'http' | 'https') { - if (!isNodeEnvironment || typeof require === 'undefined') { - return false as const; - } +// In Node we default to keep-alive agents so TCP connections are reused under concurrent load. +// Guards: `isNodeEnvironment` skips browsers; `typeof require` skips native ESM (where `require` is undefined). +// The requires use literal 'http'/'https' so bundlers (Turbopack/webpack) can statically analyze them, and the +// package.json "browser" field maps those modules to `false` so browser bundles never try to resolve them. +function isNodeRuntime(): boolean { + return isNodeEnvironment && typeof require !== 'undefined'; +} + +function createHttpAgent() { + if (!isNodeRuntime()) return false as const; + + return new (require('http').Agent)({ keepAlive: true }); +} + +function createHttpsAgent() { + if (!isNodeRuntime()) return false as const; - if (moduleName === 'http') return new (require('http').Agent)({ keepAlive: true }); return new (require('https').Agent)({ keepAlive: true }); } @@ -24,8 +33,8 @@ export function httpClient(options: HttpClientParams): AxiosInstance { headers: {} as AxiosRequestHeaders, basePath: '', proxy: false as const, - httpAgent: createKeepAliveAgent('http'), - httpsAgent: createKeepAliveAgent('https'), + httpAgent: createHttpAgent(), + httpsAgent: createHttpsAgent(), timeout: 30000, logHandler: (level: string, data?: any) => { if (level === 'error') { From 8adca8ce53ca2693e1b85ee530959093dc20a706 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 5 Aug 2026 12:34:50 +0530 Subject: [PATCH 3/4] fix: Update ESM compatibility tests and webpack config for http/https agents --- .talismanrc | 2 -- config/webpack.config.js | 9 +++++---- test/esm-compat.mjs | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.talismanrc b/.talismanrc index ef237b6..d43080e 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,6 +1,4 @@ fileignoreconfig: - filename: package-lock.json checksum: 1cba2ff4ea6e6f725f6d22bc8c2e04e46c30923986124ef7b4689c705a0397f1 -- filename: test/esm-compat.mjs - checksum: 6da8aca15e54c00fe69b0226153b5d641e6f79845cd63adf5d38059932c15ae8 version: "" \ No newline at end of file diff --git a/config/webpack.config.js b/config/webpack.config.js index 91d8c69..63683ac 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -29,10 +29,11 @@ module.exports = { }, // 'http' and 'https' are Node-only built-ins. Marking them external means: // - In Node.js (CJS/UMD): the UMD wrapper calls require('http') at runtime → real module - // - In browser: UMD wrapper looks up window['_'] → undefined, but isNodeEnvironment - // is false so createKeepAliveAgent() short-circuits before reaching require() + // - In browser: the UMD wrapper looks up window['http'] / window['https'] → undefined, but + // isNodeRuntime() is false there so createHttpAgent()/createHttpsAgent() short-circuit + // before reaching require() externals: { - http: { commonjs: 'http', commonjs2: 'http', root: '_' }, - https: { commonjs: 'https', commonjs2: 'https', root: '_' }, + http: { commonjs: 'http', commonjs2: 'http', root: 'http' }, + https: { commonjs: 'https', commonjs2: 'https', root: 'https' }, }, }; diff --git a/test/esm-compat.mjs b/test/esm-compat.mjs index d8b3aac..2d346bb 100644 --- a/test/esm-compat.mjs +++ b/test/esm-compat.mjs @@ -29,7 +29,7 @@ try { process.exit(1); } -// In native ESM, require is not defined, so createKeepAliveAgent must fall back to false +// In native ESM, require is not defined, so createHttpAgent()/createHttpsAgent() must fall back to false assert( instance.defaults.httpAgent === false, `httpAgent should be false in native ESM (got ${instance.defaults.httpAgent})` From 90e2c1fb78c9d3ac5fd971dd6a5444b2d7524276 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 5 Aug 2026 13:04:35 +0530 Subject: [PATCH 4/4] docs: Add 1.5.1 changelog entry for ESM and UMD require compatibility fix Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bf7f74..2f8b35d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## Change log +### Version: 1.5.1 +#### Date: August-05-2026 + - Fix: Resolve `require()` incompatibility that broke bundled and native ESM builds. The keep-alive agent setup used a dynamic `require(moduleName)`, which bundlers (webpack/Turbopack, e.g. Next.js) cannot statically analyze — producing `MODULE_NOT_FOUND` / "expression is too dynamic" build failures. It now uses statically analyzable `require('http')`/`require('https')` calls. + - Fix: Guard keep-alive agent creation for native ESM environments (where `require` is undefined) to prevent a `ReferenceError`. + - Fix: Add a package `browser` field mapping `http`/`https` to `false`, and externalize them in the UMD build, so browser bundles resolve cleanly. + ### Version: 1.5.0 #### Date: August-03-2026 - Fix: Classify request timeouts (`ECONNABORTED`) distinctly instead of a generic `UNKNOWN_ERROR`, preserving the real error code and message