Node configuration: import("node:fs/promises") fails

Tldr:
Why am I unable to use
const fs = await import("node:fs/promises");
In my plugin code?

Details:

I have built an Obsidian plugin using typescript, it works great. I’m now trying to add additional functionality, and am adding an additional external package that adds some functionality (PackageX).

Internally, within the code of that package (PackageX, which I cannot change), there is code like so:
const fs = await import("node:fs/promises");

This is a dynamic import, using the module ‘node:’ namespace. And this is valid syntax, but a recent addition, I think.

As soon as I try to use this external package, the build of my plugin fails with this error:

Could not resolve "node:fs/promises". You can mark the path "node:fs/promises" as external to exclude it from the bundle, which will remove this error. You can also add ".catch()" here to handle this failure at run-time instead of bundle-time.

I can indeed add “node.*” to my esbuild.config.mjs under externals as indicated above, and this resolves the build-time error, but then instead at runtime of the plugin, I get this error:

Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: node:fs/promises

This implies that when Obsidian is running, it doesn’t recognize (or use) the “node:” prefix and is unable to pull “fs/promises” from the built-in (local?) node packages.

It’s not specifically that external PackageX I’m trying to pull in, either. If I uninstall that PackageX entirely, and simply attempt to use await import(“node:fs/promises”) directly in my own plugin, it fails in the same way. I only mention PackageX because I can’t change the code and do it another way if I want to use it.

I’m relatively ok with typescript, but I can’t really unravel what is needed here. I’m using more or less the “standard” package.json, esbuild.config.mjs, tsconfig.json you see in a lot of plugins. I suspect perhaps I need to add something, or change the esbuild target, or…?

Anyone have any clues to offer, workarounds, seen this before? And also, can I be confirmed that the “platform” of esbuild here should be “browser”, not “node”?

Much appreciated.

Did you try import {x} from "packageX" ?

Yes, that static syntax works fine and I use it myself for static dependencies. The question is really why the other dynamic syntax doesn’t work, when it is perfectly valid and supported by node.

The crux here is that a popular and well-used 3rd party package uses that syntax of dynamically referencing dependencies, and because that doesn’t work within the context of Obsidian/the plugin, I can’t use the third-party package at all within my plugin. I suspect it is something about the way my plugin gets built, but I don’t know.

Further investigation led me to what I believe to be the answer here, so I’m leaving it for posterity and future readers.

The Obsidian plugin is ultimately running within a typescript platform=browser environment, and is hosted within Electron. As a browser environment and not a true node environment, it only recognizes dynamic imports with a url-based schema, not any import with a “node:” prefix.

Thus, Electron cannot interpret import(“node:fs/promises”) because it is not a native node environment, and that “url” is unintelligible to it. A dynamic import of this type will fail. This “node:” syntax is valid, in other words, just not for environments like an Obsidian plugin running within Electron.

In my specific case, this prevents me from using the third-party package within an Obsidian plugin because it internally contains this kind of dynamic import syntax. More generally: when doing Obsidian plugin development, you will not be able to use a third-party package which does a dynamic import like this because it will fail as above. If by some magic, the third party package changed to use static node imports instead of dynamic, I would expect it would work, but that’s not feasible in this situation.

There are potentially ways to address this, like using polyfills in the typescript build (“esbuild-plugin-polyfill-node” or similar) to replace these references with a local/“cached” version of the node library, but the difficulty and value of doing so is up to you.

If anyone has corrections or more information to supply, I’m more than happy to be corrected. :slight_smile:

1 Like

One more piece of information. The above is based on the default of esbuild having the platform set to “browser”. I posted another topic on the distinction between esbuild platform being set to either node or browser here, related to something else.

I have not tested/played with this too much, but possibly another solution to this import(“node:fs/promises”) problem as described above is some variation on to setting the esbuild platform=“node”. I tried it a little, but all it did was change the imported code from:
import("node:fs/promises")
into
import("fs/promises")
And otherwise still did not work, as that’s not a “url” either.

So, I suspect this is still something about the way a dynamic import statement behaves within a “browser-like environment”, no matter what platform it was compiled for. More specifically, that if an imported package internally uses import(“node:…”) it wasn’t really intended to be used in a “browser-like” environment, even if you compile it to a node platform, and maybe the authors of that package should update it.

1 Like