tschaub / mock-fs

Configurable mock for the fs module
https://npmjs.org/package/mock-fs
Other
911 stars 86 forks source link

Keeping mock-fs alive without process.binding #386

Open BadIdeaException opened 3 months ago

BadIdeaException commented 3 months ago

As seen in #383:

Nodejs also planed to cut off process.binding() from user land, that's death sentence for mock-fs.

and had started wondering if Loaders (now called Customization Hooks) might be a way to control access to the fs module without needing access to process.binding.

I've taken the liberty of throwing together a quick gist for this as a proof of concept. It provides a mechanism to switch access between real and fake file system by calling fakefs() and fakefs.restore(), resp. For this, it intercepts import resolution requests to fs or node:fs and redirects them to a dedicated module, which in turn exports a Proxy that basically acts as a switch between the real and the fake file system at will.

The only caveat with this approach that I can see is that the hooks would need to be registered right at the beginning, but this should easily be doable by specifying an import flag. Mocha even supports doing this as part of the configuration file.

I would love if this project could be kept alive. The only alternative I have found is memfs, and I dislike that I have to manually mock the fs import in the system under test literally every time.

tschaub commented 3 months ago

This sounds really promising, @BadIdeaException. I really appreciate you taking the time to investigate this. I'll try to make time in the coming days to see what it would be like to use your approach.

BadIdeaException commented 3 months ago

I'm happy to hear that. Also will be happy to help more if I can.

BadIdeaException commented 3 months ago

@tschaub Any news on this yet? I have been playing around with it a little in a local copy (I haven't forked, just cloned and went wild), and I have been able to throw this together in a way that both keeps the existing compatibility with CommonJS and does its new trick with ESM. New Gist here. Just add the files into mock-fs/lib/ (well, package.json in the project root, obviously). The tryit files I had also put in my project root, for lack of a better place.

Trying it

For convenience, I then symlinked this into node_modules, to emulate mock-fs having been npm installed. (ln -s ./ node_modules/mock-fs.) Then

node tryit.cjs
// null [ '.gitignore' ]

and

node --import mock-fs/bootstrap tryit.mjs
// null [ '.gitignore' ]

Ergo, works in both ways.

Some remarks and open questions

  1. Note the lack of a "main" field in package.json and the addition of "exports". This is how the magic of diverting ESM imports to the new system happens.
  2. Not all methods of the original fs are available. In fact, most of them aren't. (I tried readFileSync first, but nope; and the signature of readdir does not conform to the original). I suspect this has to do with the magic of how your re-binding code works in the original index.js, but to be honest, that part of your code is not very clear to me. I was under the impression you had (mostly) re-implemented the methods of node:fs in Binding, but this seems to not be the case? Am I overlooking something here? This seems to me to be the biggest remaining obstacle.
  3. This is obviously still pretty rough around the edges, but closer to the "real deal" than before. It's definitely missing guard rails concerning the importing node version - module.register only became available with Node 18, although hook.mjs looks like it should be compatible to being used as a loader. ( I haven't tried this, though.)
  4. I dislike mixing CommonJS (.js) and ESM (.mjs) in this way, but short of rewriting everything in ESM I can't see a way around it. The import hook mechanism seems to rely on ESM (which I guess sort of makes sense), and I haven't found a way around this.
  5. In a similar vein, at the moment, CJS and ESM versions are largely independent of each other. In particular, they do not share an implementation of the mocking function. For the time being, I simply exported the existing index.js as the CJS version, then wrote a rough-shod ESM one next to it (although it does use the same implementation of Binding and FileSystem). My instinct is that the existing code should be fairly easy to refactor in such a way that the two share code for their common functionality, though.
  6. The "tests" (if you can call it that) are obviously somewhat lacking in coverage... :laughing: I haven't bothered with writing an actual test suite for the time being, because I'm expecting difficulties trying to have CJS and ESM tests in the same test suite. This will be a problem for a later time though.
  7. Do you know of a better way to exchange code than gists? I guess I could fork, but I don't see this being ready for a PR all that soon. Or if I forked, could I give you and @3cp write privileges? I haven't collaborated much on Github...

Would love to hear your thoughts.