Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESM loader in v11 does not work with got package, after installing the latest import-in-the-middle #1920

Closed
corevo opened this issue Dec 20, 2023 · 25 comments
Assignees
Labels

Comments

@corevo
Copy link

corevo commented Dec 20, 2023

Description

Attempting to use v11 in an ESM project that includes got causes the node process to crash.

In Node v18 you will get a silent exit code 13. In Node v20, it will throw a SyntaxError before exit code 1.

app-1  | (node:8) ExperimentalWarning: `--experimental-loader` may be removed in the future; instead use `register()`:
app-1  | --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("newrelic/esm-loader.mjs", pathToFileURL("./"));'
app-1  | (Use `node --trace-warnings ...` to show where the warning was created)
app-1  | file:///app/node_modules/@payzen/got-commons/node_modules/got/dist/source/index.js?iitm=true:39
app-1  |     let $default = _.default
app-1  |         ^
app-1  |
app-1  | SyntaxError: Identifier '$default' has already been declared
app-1  |     at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:155:18)
app-1  |     at callTranslator (node:internal/modules/esm/loader:285:14)
app-1  |     at ModuleLoader.moduleProvider (node:internal/modules/esm/loader:291:30)
app-1  |
app-1  | Node.js v20.10.0
app-1 exited with code 1

Expected Behavior

Running node --loader newrelic/esm-loader.mjs should work without crashing node.
NOTE: # ( Tell us what you expected to happen. )

Troubleshooting or NR Diag results

Steps to Reproduce

  1. run npm init -y
  2. run npm i --save got newrelic
  3. create the file index.mjs with the contents
import got from 'got'
  1. run node --loader newrelic/esm-loader.mjs index.mjs
  2. crash

Your Environment

  • ex: Browser name and version: not relevant
  • ex: Node version: 20.10.0
  • ex: Operating System and version: Linux

Additional context

@workato-integration
Copy link

@jsumners-nr
Copy link
Contributor

jsumners-nr commented Dec 20, 2023

What version of the agent are you using? What version of import-in-the-middle is being used? The output of npm ls newrelic import-in-the-middle got should help us proceed.

@zintus
Copy link

zintus commented Dec 20, 2023

zintus/newrelic-node-examples@c209a0d
I believe this repro is related. Please advise

npm run start fails
running node index.js succeeds

Node v21.3.0

@corevo
Copy link
Author

corevo commented Dec 21, 2023

  └─┬ newrelic@11.7.0
    └── import-in-the-middle@1.7.2

@jsumners-nr
Copy link
Contributor

Thank you. I have narrowed down the issue and submitted a PR upstream (nodejs/import-in-the-middle#53).

@jsumners-nr jsumners-nr self-assigned this Dec 21, 2023
@ljharb
Copy link

ljharb commented Dec 21, 2023

You can't export default multiple times, so it's possible this is a node loader bug.

@jsumners-nr
Copy link
Contributor

Thank you @ljharb. It's a problem with IITM. I wrote a test for core and it passes:

Core Test
diff --git a/test/es-module/test-esm-initialization.mjs b/test/es-module/test-esm-initialization.mjs
index f03a47d5d3..e7882e0839 100644
--- a/test/es-module/test-esm-initialization.mjs
+++ b/test/es-module/test-esm-initialization.mjs
@@ -28,3 +28,19 @@ describe('ESM: ensure initialization happens only once', { concurrency: true },
     assert.strictEqual(code, 0);
   });
 });
+
+describe('ESM: nested default exports are handled correctly', () => {
+  it(async() => {
+    const { code, stderr, stdout } = await spawnPromisified(execPath, [
+      '--experimental-import-meta-resolve',
+      '--loader',
+      fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
+      '--no-warnings',
+      fixtures.path('es-modules', 'nested-default', 'index.mjs'),
+    ]);
+
+    assert.strictEqual(stderr, '');
+    assert.strictEqual(stdout.match(/resolve passthru/g)?.length, 3);
+    assert.strictEqual(code, 0);
+  });
+});
diff --git a/test/fixtures/es-modules/nested-default/a.mjs b/test/fixtures/es-modules/nested-default/a.mjs
new file mode 100644
index 0000000000..dc07e2033d
--- /dev/null
+++ b/test/fixtures/es-modules/nested-default/a.mjs
@@ -0,0 +1,3 @@
+const a = 'a'
+export default a
+export * from './b.mjs'
diff --git a/test/fixtures/es-modules/nested-default/b.mjs b/test/fixtures/es-modules/nested-default/b.mjs
new file mode 100644
index 0000000000..7f5dadf656
--- /dev/null
+++ b/test/fixtures/es-modules/nested-default/b.mjs
@@ -0,0 +1,2 @@
+const b = 'b'
+export default b
diff --git a/test/fixtures/es-modules/nested-default/index.mjs b/test/fixtures/es-modules/nested-default/index.mjs
new file mode 100644
index 0000000000..755d201fc8
--- /dev/null
+++ b/test/fixtures/es-modules/nested-default/index.mjs
@@ -0,0 +1 @@
+export * from './a.mjs'

@zintus
Copy link

zintus commented Dec 23, 2023

I can confirm your fix helps with my repro too. Thanks for the investigation @jsumners-nr

@corevo
Copy link
Author

corevo commented Dec 24, 2023

I've tried your fix and the error is now different, it looks like the module loading is okay, but not all the exports are re-exported correctly, it looks like the exported object is got's Request rather than the got object itself, I'm using got@14.0.0 which is a native ESM module.

TypeError: got.extend is not a function
    at file:///app/node_modules/@payzen/got-commons/build/got-commons.js:185:29
    at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadESM (node:internal/process/esm_loader:28:7)
    at async handleMainPromise (node:internal/modules/run_main:120:12)

Printing the default exports yields

[class Request extends Duplex]

@jsumners-nr
Copy link
Contributor

Thank you for the update. Figuring out the full solution given that information will be tricky indeed.

@jsumners-nr
Copy link
Contributor

I have determined the underlying problem. It is going to require some significant work in IITM.

@jsumners-nr
Copy link
Contributor

The issue is that in https://github.com/DataDog/import-in-the-middle/blob/ae292fb72d2a3444f3c4fa77439be81df49ff8f9/hook.js#L205-L212 we merge all transitive exports into a singular namespace. Whatever was the last "default" export in derived namespaces ends up being the default exported from the shim module. Which means we need to figure out how to provide all of the required exports from the shim module while also picking the primary default and renaming the others according to their module name (e.g. './foo-bar.js' default export should be renamed and exported as fooBar).

I'm at a loss for how to accomplish this. I welcome any ideas.

@corevo
Copy link
Author

corevo commented Dec 28, 2023

Sorry but I think I don't fully understand the issue here.

Using the keyword default more than once is illegal, it will return a parsing error of duplicate definition of .default on the module object, are you referring to a module like this

/* first.mjs */
export default 1

/* second.mjs */
export default 2

/* index.mjs */
export * from './first.mjs'
export * from './second.mjs'

Because if so then in index.mjs the .default is not defined because the default keyword was not strictly used and the exported module will be {}, unless you want the default and then you have to rename it if imported from more than one module.

@ljharb
Copy link

ljharb commented Dec 28, 2023

export * from doesn’t bring the default export, so there shouldn’t ever be more than one.

@jsumners-nr
Copy link
Contributor

After my last update I had a brainwave and seem to have a viable solution. I'm currently working through cleaning it up and expanding my test to try and cover all bases.

@jsumners-nr
Copy link
Contributor

I believe commit nodejs/import-in-the-middle@1abc010 solves the problem.

bengl pushed a commit to nodejs/import-in-the-middle that referenced this issue Jan 8, 2024
Ref: newrelic/node-newrelic#1920

<strike>👋 my last "fix" (#43) broke in a new way thanks to the helpful
ESM allowance of exporting `default` multiple times. This PR resolves
the issue. I suspect there could be further problems with the exports
since I have no idea how exporting `default` multiple times is meant to
be interpreted, but I suspect any such issues will be an extreme edge
case.

The only other thing I can think of to do here is to somehow track that
we already have a `default` export and munge the names of any subsequent
ones.</strike>

-----

I have since learned that ESM doesn't actually allow exporting `default`
multiple times. Transitive `default` exports get mapped to some other
name for the module that has imported them, and only the directly
imported `default` gets exported. Still, we have to handle figuring out
which `default` is the right one and construct our shim namespace
accordingly.

-----

2023-01-03: this can only be supported on Node versions where we parse
the modules with an AST parser and interpret the code manually. So I
have updated the test suite for this feature to only run on such
versions.

---------

Co-authored-by: James Sumners <git@zifbang.com>
@jsumners-nr
Copy link
Contributor

I think we should see a release soon, but in the meantime, if you'd like to try the latest main branch of import-in-the-middle it should solve the issue.

@alexey-yarmosh
Copy link

Hey, thanks for the fix. I've just checked with the new version of import-in-the-middle. It fixes got issue.

In the meantime it still fails with another import in our project. When I try to:

import { defineScript } from 'redis';

I get:

node --loader newrelic/esm-loader.mjs test.mjs
(node:97636) ExperimentalWarning: `--experimental-loader` may be removed in the future; instead use `register()`:
--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("newrelic/esm-loader.mjs", pathToFileURL("./"));'
(Use `node --trace-warnings ...` to show where the warning was created)
import { defineScript } from 'redis';
         ^^^^^^^^^^^^
SyntaxError: The requested module 'redis' does not provide an export named 'defineScript'
    at ModuleJob._instantiate (node:internal/modules/esm/module_job:132:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:214:5)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadESM (node:internal/process/esm_loader:34:7)
    at async handleMainPromise (node:internal/modules/run_main:113:12)

Node.js v20.10.0

Also seems problem is specificly with defineScript, cause import { createClient } from 'redis'; works fine.

@barryhagan
Copy link

In the meantime it still fails with another import in our project. When I try to:
...
Also seems problem is specificly with defineScript, cause import { createClient } from 'redis'; works fine.

This is because of the re-export issue. Only defineScript is a re-export, not createClient.

We've been waiting for some time for this to be able to move to version 11 and it looks like they finally merged the 6 month old PR.

@jsumners-nr
Copy link
Contributor

IITM 1.7.3 has been published and resolves the original issue described here. Please update your dependencies to get the fix. If there are other problems, please open a new issue.

@alexey-yarmosh
Copy link

Hey, do you have plans/approximate dates to update iitm version in node-newrelic itself?

@jsumners-nr
Copy link
Contributor

The update is covered by the semver range qualifier specified in the package.json:

"import-in-the-middle": "^1.6.0",

Updating your dependencies will net the fix.

@alexey-yarmosh
Copy link

Thanks!

@zintus
Copy link

zintus commented Mar 18, 2024

Thanks for working on this! @jsumners-nr

My repro i've linked before still fails with same steps. Please advise
newrelic/newrelic-node-examples@main...zintus:newrelic-node-examples:main


> esm-app@1.0.0 start
> node --experimental-loader newrelic/esm-loader.mjs -r newrelic index.js

(node:36337) ExperimentalWarning: `--experimental-loader` may be removed in the future; instead use `register()`:
--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("newrelic/esm-loader.mjs", pathToFileURL("./"));'
(Use `node --trace-warnings ...` to show where the warning was created)
file:///Users/zintus/workspace/newrelic-node-examples/esm-app/node_modules/openai/resources/index.mjs?iitm=true:55
    let $Completions = namespace.Completions
        ^

SyntaxError: Identifier '$Completions' has already been declared
    at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:167:18)
    at callTranslator (node:internal/modules/esm/loader:285:14)
    at ModuleLoader.moduleProvider (node:internal/modules/esm/loader:291:30)

@jsumners-nr
Copy link
Contributor

There have been a few new releases of import-in-the-middle. Does your issue still persist, @zintus ?

@bizob2828 bizob2828 moved this to Done: Issues recently completed in Node.js Engineering Board Jul 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Archived in project
Development

No branches or pull requests

6 participants