Skip to content

Commit d4d6b5d

Browse files
authored
Merge pull request #249 from contentstack/development
DX | 05-08-2026 | Release
2 parents bc7df74 + 969b66f commit d4d6b5d

8 files changed

Lines changed: 125 additions & 11 deletions

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
## Change log
22

3+
### Version: 1.5.1
4+
#### Date: August-05-2026
5+
- 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.
6+
- Fix: Guard keep-alive agent creation for native ESM environments (where `require` is undefined) to prevent a `ReferenceError`.
7+
- Fix: Add a package `browser` field mapping `http`/`https` to `false`, and externalize them in the UMD build, so browser bundles resolve cleanly.
8+
39
### Version: 1.5.0
410
#### Date: August-03-2026
511
- Fix: Classify request timeouts (`ECONNABORTED`) distinctly instead of a generic `UNKNOWN_ERROR`, preserving the real error code and message

‎config/webpack.config.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,13 @@ module.exports = {
2727
resolve: {
2828
extensions: ['.ts', '.js', '.tsx', '.jsx'],
2929
},
30+
// 'http' and 'https' are Node-only built-ins. Marking them external means:
31+
// - In Node.js (CJS/UMD): the UMD wrapper calls require('http') at runtime → real module
32+
// - In browser: the UMD wrapper looks up window['http'] / window['https'] → undefined, but
33+
// isNodeRuntime() is false there so createHttpAgent()/createHttpsAgent() short-circuit
34+
// before reaching require()
35+
externals: {
36+
http: { commonjs: 'http', commonjs2: 'http', root: 'http' },
37+
https: { commonjs: 'https', commonjs2: 'https', root: 'https' },
38+
},
3039
};

‎package-lock.json‎

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

‎package.json‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
{
22
"name": "@contentstack/core",
3-
"version": "1.5.0",
3+
"version": "1.5.1",
44
"type": "commonjs",
55
"main": "./dist/cjs/src/index.js",
66
"types": "./dist/cjs/src/index.d.ts",
7+
"browser": {
8+
"http": false,
9+
"https": false
10+
},
711
"exports": {
812
".": {
913
"import": {

‎src/lib/contentstack-core.ts‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,24 @@ import { ERROR_MESSAGES } from './error-messages';
66

77
const isNodeEnvironment = typeof window === 'undefined';
88

9-
// Guarded require: keeps 'http'/'https' out of browser bundles, which have no browser field of their own to redirect this.
10-
function createKeepAliveAgent(moduleName: 'http' | 'https') {
11-
if (!isNodeEnvironment) {
12-
return false as const;
13-
}
9+
// In Node we default to keep-alive agents so TCP connections are reused under concurrent load.
10+
// Guards: `isNodeEnvironment` skips browsers; `typeof require` skips native ESM (where `require` is undefined).
11+
// The requires use literal 'http'/'https' so bundlers (Turbopack/webpack) can statically analyze them, and the
12+
// package.json "browser" field maps those modules to `false` so browser bundles never try to resolve them.
13+
function isNodeRuntime(): boolean {
14+
return isNodeEnvironment && typeof require !== 'undefined';
15+
}
16+
17+
function createHttpAgent() {
18+
if (!isNodeRuntime()) return false as const;
19+
20+
return new (require('http').Agent)({ keepAlive: true });
21+
}
22+
23+
function createHttpsAgent() {
24+
if (!isNodeRuntime()) return false as const;
1425

15-
return new (require(moduleName).Agent)({ keepAlive: true });
26+
return new (require('https').Agent)({ keepAlive: true });
1627
}
1728

1829
export function httpClient(options: HttpClientParams): AxiosInstance {
@@ -22,8 +33,8 @@ export function httpClient(options: HttpClientParams): AxiosInstance {
2233
headers: {} as AxiosRequestHeaders,
2334
basePath: '',
2435
proxy: false as const,
25-
httpAgent: createKeepAliveAgent('http'),
26-
httpsAgent: createKeepAliveAgent('https'),
36+
httpAgent: createHttpAgent(),
37+
httpsAgent: createHttpsAgent(),
2738
timeout: 30000,
2839
logHandler: (level: string, data?: any) => {
2940
if (level === 'error') {

‎test/contentstack-core.node-agent.spec.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*/
44
import http from 'http';
55
import https from 'https';
6+
import { execFile } from 'child_process';
7+
import * as path from 'path';
68
import { httpClient } from '../src/lib/contentstack-core';
79

810
describe('httpClient default connection agents (Node environment)', () => {
@@ -19,4 +21,18 @@ describe('httpClient default connection agents (Node environment)', () => {
1921
expect(instance.defaults.httpsAgent).toBeInstanceOf(https.Agent);
2022
expect((instance.defaults.httpsAgent as any).keepAlive).toBe(true);
2123
});
24+
25+
it('should not throw and fall back to false agents in native ESM (require is not defined)', (done) => {
26+
// Runs test/esm-compat.mjs as a native ESM child process.
27+
// Regression test for contentstack-js-core#246: ReferenceError: require is not defined.
28+
// Requires `npm run build` to have been run first.
29+
const script = path.join(__dirname, 'esm-compat.mjs');
30+
execFile(process.execPath, [script], (error, stdout, stderr) => {
31+
if (error) {
32+
done(new Error(`Native ESM test failed:\n${stderr || stdout}`));
33+
} else {
34+
done();
35+
}
36+
});
37+
});
2238
});

‎test/esm-compat.mjs‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Native ESM integration test for @contentstack/core.
3+
*
4+
* Run after `npm run build`:
5+
* node test/esm-compat.mjs
6+
*
7+
* Verifies that importing httpClient via ESM (where require is not defined)
8+
* does not throw a ReferenceError and falls back to false for http/https agents.
9+
* This catches the regression reported in contentstack-js-core#246.
10+
*/
11+
import { httpClient } from '../dist/esm/src/index.js';
12+
13+
let failed = false;
14+
15+
function assert(condition, message) {
16+
if (!condition) {
17+
process.stderr.write(`FAIL: ${message}\n`);
18+
failed = true;
19+
} else {
20+
process.stdout.write(`PASS: ${message}\n`);
21+
}
22+
}
23+
24+
let instance;
25+
try {
26+
instance = httpClient({});
27+
} catch (err) {
28+
process.stderr.write(`FAIL: httpClient() threw in native ESM: ${err.message}\n`);
29+
process.exit(1);
30+
}
31+
32+
// In native ESM, require is not defined, so createHttpAgent()/createHttpsAgent() must fall back to false
33+
assert(
34+
instance.defaults.httpAgent === false,
35+
`httpAgent should be false in native ESM (got ${instance.defaults.httpAgent})`
36+
);
37+
38+
assert(
39+
instance.defaults.httpsAgent === false,
40+
`httpsAgent should be false in native ESM (got ${instance.defaults.httpsAgent})`
41+
);
42+
43+
if (failed) process.exit(1);
44+
process.stdout.write('ESM compat: all checks passed\n');

‎test/esm-exports.spec.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,30 @@ describe('ESM Exports Tests', () => {
106106
});
107107
});
108108

109+
describe('ESM Build - require() Safety (Turbopack/webpack compatibility)', () => {
110+
const esmCorePath = path.join(distPath, 'esm', 'src', 'lib', 'contentstack-core.js');
111+
112+
it('should have ESM contentstack-core.js built', () => {
113+
expect(fs.existsSync(esmCorePath)).toBe(true);
114+
});
115+
116+
it('should not contain dynamic require(variable) expressions that webpack/Turbopack reject', () => {
117+
const content = fs.readFileSync(esmCorePath, 'utf-8');
118+
// Dynamic require(identifier) — bundlers cannot statically trace these and throw
119+
// "Cannot find module as expression is too dynamic"
120+
expect(content).not.toMatch(/require\([a-zA-Z_$]/);
121+
});
122+
123+
it('should use only static string-literal require calls', () => {
124+
const content = fs.readFileSync(esmCorePath, 'utf-8');
125+
const requireCalls = [...content.matchAll(/require\(([^)]+)\)/g)].map((m) => m[1].trim());
126+
for (const arg of requireCalls) {
127+
// Every require() arg must be a quoted string literal
128+
expect(arg).toMatch(/^['"`]/);
129+
}
130+
});
131+
});
132+
109133
describe('Source Code Imports', () => {
110134
it('should be able to import getData as named export from source', async () => {
111135
const { getData } = await import('../src');

0 commit comments

Comments
 (0)