node icon indicating copy to clipboard operation
node copied to clipboard

createRequire does not respect deleting module from require.cache

Open sdegutis opened this issue 8 months ago • 13 comments

Version

v23.10.0

Platform

Microsoft Windows NT 10.0.26100.0 x64

Subsystem

No response

What steps will reproduce the bug?

mod.js

console.log('in mod.ts')

testcase.js

import { createRequire } from 'module'
const require = createRequire(import.meta.url)

require('./mod.js?1')

const key = Object.keys(require.cache)[0]
delete require.cache[key]

require('./mod.js?2')

How often does it reproduce? Is there a required condition?

Every time.

What is the expected behavior? Why is that the expected behavior?

in mod.ts
in mod.ts

What do you see instead?

in mod.ts

Additional information

I traced it to this line, which just fully ignores source.

https://github.com/nodejs/node/blob/eab0fe264bf5e7e105827651bbb6a895354aa9c3/lib/internal/modules/esm/loader.js#L390-L391

sdegutis avatar Mar 31 '25 16:03 sdegutis

For context, this is after I provided source via a custom sync loader which had both resolve and load hooks. I traced the source all the way to this point, and Node.js does not take into account that I had provided a custom source. That's why I consider this a bug.

sdegutis avatar Mar 31 '25 22:03 sdegutis

Also, I'm using a cache busting query already, but it gets stripped away a few functions up when turned into a filename for the sake of requiring it via CJS resolution, so it's apparently ignored.

sdegutis avatar Apr 01 '25 09:04 sdegutis

require only takes file paths or bare specifiers, not URLs, so no query strings would make sense there.

ljharb avatar Apr 02 '25 03:04 ljharb

@ljharb right but it's a weird hybrid of require and import, because it's in an ESM project and it's an ESM file being imported, but require is called via createRequire, which goes through the hassle of resolving it as if required, but skips require's actual resolution which would use require.cache (Module._cache), and ultimately finds an ESM module, sees that it's already been resolved, and uses that one's exports. Ironically, it's really only using require-logic to skip the cache busting, but in every other area in this code it's acting as if it's ESM. So it's inconsistent. Either it should fully use ESM resolution and see the cache busting query, or it should fully use require-logic and see that I deleted it from require.cache.

sdegutis avatar Apr 02 '25 09:04 sdegutis

I’m confused, if you want to skip the cache busting just import it normally. The only place it makes any sense to require ESM is inside a CJS file.

ljharb avatar Apr 02 '25 14:04 ljharb

@ljharb In this case, I'm using createRequire because it's a sync ESM module (no tlawait) and I'd like to avoid poisining all sync methods involved

sdegutis avatar Apr 02 '25 14:04 sdegutis

I’m still confused - what do you mean by poisoning?

ljharb avatar Apr 02 '25 14:04 ljharb

https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/

sdegutis avatar Apr 02 '25 14:04 sdegutis

Interesting, that article gives me a new respect for Go.

sdegutis avatar Apr 02 '25 14:04 sdegutis

ahhh this is something that can't be a static import at the root of the file?

ljharb avatar Apr 02 '25 15:04 ljharb

@ljharb Right, it's dynamically imported/required in a sync context.

sdegutis avatar Apr 02 '25 15:04 sdegutis

require('./foo.mjs') also does not honor cache invalidation.

Anyone have a workaround? I'm trying to migrate a module to ESM while providing backward compat and this is breaking tests.

jdmarshall avatar Aug 16 '25 06:08 jdmarshall