Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,13 @@ 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: 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: 'http' },
https: { commonjs: 'https', commonjs2: 'https', root: 'https' },
},
};
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
{
"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",
"browser": {
"http": false,
"https": false
},
"exports": {
".": {
"import": {
Expand Down
27 changes: 19 additions & 8 deletions src/lib/contentstack-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,24 @@ 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.
function createKeepAliveAgent(moduleName: 'http' | 'https') {
if (!isNodeEnvironment) {
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;

return new (require(moduleName).Agent)({ keepAlive: true });
return new (require('https').Agent)({ keepAlive: true });
}

export function httpClient(options: HttpClientParams): AxiosInstance {
Expand All @@ -22,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') {
Expand Down
16 changes: 16 additions & 0 deletions test/contentstack-core.node-agent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)', () => {
Expand All @@ -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();
}
});
});
});
44 changes: 44 additions & 0 deletions test/esm-compat.mjs
Original file line number Diff line number Diff line change
@@ -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 createHttpAgent()/createHttpsAgent() 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');
24 changes: 24 additions & 0 deletions test/esm-exports.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading