elastic / elastic/apm-agent-nodejs
ESM loader causes entrypoint import.meta.main to be false
- 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
Assessment
This issue has not been assessed yet.