node icon indicating copy to clipboard operation
node copied to clipboard

configure CJS named export within ESM loader

Open loynoir opened this issue 1 year ago β€’ 4 comments

What is the problem this feature will solve?

When ESM loader resolve and load a virtual CJS module, named export not found.

Reproduce Description

  • use ESM loader to load a virtual CJS module

  • the virtual CJS name is virtual:reproduce

  • the virtual CJS code is module.exports.foo = () => 42;

  • Expect: OK

  • Actual: got Named export 'foo' not found

Workaround

Add virtual ESM module using code export const foo = CJSExports.foo

But it will

  • at least double the complexity of CJS part of importHook.resolve

  • at least double the complexity of CJS part of importHook.load

So, I think better to introduce

-    return { format: 'commonjs', shortCircuit: true, source: undefined }
+    return { format: 'commonjs', shortCircuit: true, source: undefined, namedExports: ['foo'] }

Reproduce

reproduce.mjs

import { foo } from "virtual:reproduce";

// import mod from 'virtual:reproduce'
// const { foo } = mod

console.log({ foo })

import-hook.mjs

const VIRTUAL_FILEURL = 'file:///virtual/reproduce'

export async function resolve(specifier, context, nextResolve) {
  if (specifier === 'virtual:reproduce') {
    return { format: 'commonjs', shortCircuit: true, url: VIRTUAL_FILEURL }
  }

  return await nextResolve(specifier, context)
}

export async function load(url, context, nextLoad) {
  if (url === VIRTUAL_FILEURL) {
    return { format: 'commonjs', shortCircuit: true, source: undefined }
  }

  return await nextLoad(url, context)
}

require-hook.cjs

const { Module } = require('node:module')
// const { syncBuiltinESMExports } = require("node:module");
const $resolveFilename = Module._resolveFilename
const $handler = Module._extensions['.js']

const VIRTUAL_PATH = '/virtual/reproduce'
const VIRTUAL_CODE = `module.exports.foo = () => 42;`

Module._resolveFilename = function (request, parent, isMain, options) {
  if (request === VIRTUAL_PATH) {
    return VIRTUAL_PATH
  }

  return $resolveFilename(request, parent, isMain, options)
}

Module._extensions['.js'] = function (module, filename) {
  if (filename === VIRTUAL_PATH) {
    module._compile(VIRTUAL_CODE, filename)
    // syncBuiltinESMExports();
    return
  }

  $handler(module, filename)
}

Actual

$ cd `mktemp -d`
$ tree
.
β”œβ”€β”€ reproduce.mjs
β”œβ”€β”€ import-hook.mjs
└── require-hook.cjs

1 directory, 3 files
$ node --require ./require-hook.cjs --loader ./import-hook.mjs ./reproduce.mjs 
(node:687343) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
file:///path/to/reproduce.mjs:1
import { foo } from "virtual:reproduce";
         ^^^
SyntaxError: Named export 'foo' not found. The requested module 'virtual:reproduce' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'virtual:reproduce';
const { foo } = pkg;

    at ModuleJob._instantiate (node:internal/modules/esm/module_job:124:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:190:5)

Node.js v18.15.0

Expected

No error.

What is the feature you are proposing to solve the problem?

Introduce .namedExports when format is commonjs

diff --git a/import-hook.mjs b/import-hook.mjs
index c454228..63b2d12 100644
--- a/import-hook.mjs
+++ b/import-hook.mjs
@@ -10,7 +10,7 @@ export async function resolve(specifier, context, nextResolve) {
 
 export async function load(url, context, nextLoad) {
   if (url === VIRTUAL_FILEURL) {
-    return { format: 'commonjs', shortCircuit: true, source: undefined }
+    return { format: 'commonjs', shortCircuit: true, source: undefined, namedExports: ['foo'] }
   }
 
   return await nextLoad(url, context)

What alternatives have you considered?

No response

loynoir avatar May 07 '23 23:05 loynoir

@nodejs/loaders

aduh95 avatar May 14 '23 05:05 aduh95

Are you sure it’s not just that named exports aren’t found, but rather that the source is undefined? See https://github.com/nodejs/loaders#milestone-2-stability:~:text=Support%20loading%20source%20when%20the%20return%20value%20of%20load%20has%20format%3A%20%27commonjs%27

GeoffreyBooth avatar May 14 '23 16:05 GeoffreyBooth

I've also run afoul of this, but in my case the source very much is there, it's just that I can't explicitly define in the __esModule way because they aren't known at load time (I'm doing dependency injection for tests). Is there any way to disable the static analysis?

Twipped avatar Nov 03 '23 00:11 Twipped

There has been no activity on this feature request for 5 months. To help maintain relevant open issues, please add the https://github.com/nodejs/node/labels/never-stale label or close this issue if it should be closed. If not, the issue will be automatically closed 6 months after the last non-automated comment. For more information on how the project manages feature requests, please consult the feature request management document.

github-actions[bot] avatar May 01 '24 01:05 github-actions[bot]