Skip to content

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

@MartinKolarik

Description

@MartinKolarik

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:

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:

console.log(import.meta.main)

Run:

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

Observed:

false

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

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

Control cases:

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:

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

should observe:

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
  • Starting node with -r elastic-apm-node/start

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

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:

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:

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions