elastic / elastic/apm-agent-nodejs

ESM loader causes entrypoint import.meta.main to be false

Open
#5,077 1 comment 0 reactions 0 assignees View on GitHub
community triage
Dominant language
JavaScript
Stars
594
Forks
244
Avg merge
1d 8h
Merged PRs (30d)
16

Description

**Describe the bug**

When an application is started with Elastic APM's ESM loader, the entry module observes `import.meta.main === false`.

This differs from plain Node.js execution and from starting the agent with only `-r elastic-apm-node/start`. An application that uses `import.meta.main` to guard its startup code can evaluate the entry module without running the guarded startup path.

For example, an ESM entrypoint might use this pattern:

```js
if (import.meta.main) {
server.listen(...)
}
```

With the Elastic loader active, the condition is false, so guarded entrypoint startup code does not run.

**To Reproduce**

Given `main.js`:

```js
console.log(import.meta.main)
```

Run:

```sh
node --experimental-loader elastic-apm-node/loader.mjs main.js
```

Observed:

```sh
false
```

The same happens when using both the ESM loader and the preload entrypoint:

```sh
node --experimental-loader elastic-apm-node/loader.mjs -r elastic-apm-node/start.js main.js
# false
```

Control cases:

```sh
node main.js
# true

node -r elastic-apm-node/start.js main.js
# true
```

A no-op passthrough loader also preserves `import.meta.main === true`, so this appears to be caused by the Elastic/import-in-the-middle loader behavior rather than by Node's `--experimental-loader` option alone.

**Expected behavior**

An ESM entrypoint run as:

```sh
node --experimental-loader elastic-apm-node/loader.mjs main.js
```

should observe:

```js
import.meta.main === true
```

unless the loader intentionally replaces the entry module in a way that makes preserving Node's main-module semantics impossible.

**Environment (please complete the following information)**

- OS: Linux/Windows
- Node.js version: v22.22.3
- APM Server version: not relevant; failure happens before intake
- Agent version: current `main` branch / `4.15.0` package metadata

**How are you starting the agent? (please tick one of the boxes)**

- [ ] Calling `agent.start()` directly (e.g. `require('elastic-apm-node').start(...)`)
- [ ] Requiring `elastic-apm-node/start` from within the source code
- [x] Starting node with `-r elastic-apm-node/start`

This also reproduces when using both the ESM loader and preload entrypoint:

```sh
node --experimental-loader elastic-apm-node/loader.mjs -r elastic-apm-node/start.js main.js
```

**Additional context**

Root cause finding:

`loader.mjs` currently re-exports `import-in-the-middle/hook.mjs`:

```js
export * from 'import-in-the-middle/hook.mjs'
```

`import-in-the-middle` wraps modules by resolving them to a URL with an `iitm` query parameter and then loading a generated wrapper module. For the process entrypoint, Node marks the generated wrapper as the main module. The user's real entry module is then imported by the wrapper, so it sees `import.meta.main === false`.

Proposed fix:

Wrap the IITM loader exports in `elastic-apm-node/loader.mjs` and bypass IITM only for top-level entrypoint resolution, while delegating all non-entry resolves and loader hooks to IITM:

```js
import * as iitmHook from 'import-in-the-middle/hook.mjs'

export const initialize = iitmHook.initialize
export const load = iitmHook.load
export const getFormat = iitmHook.getFormat
export const getSource = iitmHook.getSource

export function resolve(specifier, context, nextResolve) {
if (!context.parentURL) {
return nextResolve(specifier, context, nextResolve)
}

return iitmHook.resolve(specifier, context, nextResolve)
}
```

Agent config options:

Click to expand

```
N/A
```

`package.json` dependencies:

Click to expand

```
N/A
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.