Tip: load an ES module in the js file of a custom node

Hi folks,

Just sharing something I found. Note that I have no clue at the moment whether this works for all libraries. Don't have enough free time to figure out stuff like that. So if anybody else knows more about the topic or knows a better solution, please share your knowledge.

I needed to use the svglint npm package in the backend js file of one of my custom nodes. However that library is an ES (EcmaScript) module and no classic CommonJs module.

  • When I import the ES module in my js file:

    import SVGLint from "svglint"
    

    Then that fails:

    SyntaxError: Cannot use import statement outside a module

  • When I load it like a classic CommonJs module:

    const SVGLint = require('svglint')
    

    Then that also fails:

    Error [ERR_REQUIRE_ESM]: require() of ES Module .../node_modules/svglint/node_modules/chalk/source/index.js from .../node_modules/svglint/src/svglint.cjs not supported.
    Instead change the require of index.js in .../node_modules/svglint/src/svglint.cjs to a dynamic import() which is available in all CommonJS modules. (line:8)

  • So after some trial and error I managed to do a dynamic import, like suggested in the previous error message:

    let SVGLint
    import('svglint').then(module => {
       //console.log(module)
       SVGLint = module.default
    })
    .catch(error => {
        console.error("Cannot import svglint dynamically:", error)
    });
    

    Then I can use the library as describe in their readme page without problems:

    const linting = await SVGLint.lintSource("...", {...})
    linting.on("done", () => { ... })
    

    Note that I used module.default after I had uncommented the console.log statement above, which revealed this information to me in the Node-RED console log:

    [Module: null prototype] {
    default: {
    lintSource: [AsyncFunction: lintSource],
    lintFile: [AsyncFunction: lintFile]
    }
    }

Hopefully this can be of any help to other developers...

Bart

Thanks Bart, not sure if you saw my Lounge post the other day about this.

Node-RED actually already uses this approach in core.

The big issue is that dynamic imports are async. That can often mean that you need to reorganise your code to be async back up a chain of function calls. That can require very extensive changes to existing code.

But yes, when you can cope with that issue, it absolutely works.

For the future, there is now an experimental feature in the new Node.js v22 that will allow synchronous requires of ESM's - long overdue.

There are some serious gotya's when working with CJS modules in ESM's as well - mostly around how different import is from require().

Safe to say that the move from CJS to ESM (which does need to happen since ESM is now the JavaScript standard and works in the browser as well) is a MESS in Node.js. :frowning:


What I'd like to eventually do is create a test Node that uses ESM for libraries and dependencies to see what impact that might have on the top-level code.

I've already a lot of async code now in my nodes due to the handling of Typed Inputs so possibly now less of an issue at least for new nodes. Certainly I don't seem to have any issues making the input code async.

Not really... Do you mean the Discourse Lounge? Long time ago that I received that. Must have been kicked out of it in that case...

Ah I wasn't aware of that. Didn't have a look in the core for this one...
I know that this has been discussed it a number of times in the past, but wasn't clear anymore to me if someone had shared a solution/workaround already.

Mind blowing that they didn't provide something like that from day one.
Like you say it has become a complete mess.
When a lib switches from CommonJs to ES, I see advices all over the place to keep using the last unmaintained CommonJs version. Not really a solution for the problem...

Thanks for the feedback!

1 Like

Indeed. I had/have a couple of those in uibuilder dependencies. Though now I'm preparing v7, I've already managed to get rid of them.

The longer I deliver in this space, the more and more determined I get to avoid dependencies and frameworks. I'm down to just 7 dependencies now (and no frameworks of course!) of which one is my module anyway. fs-extra is my next target and I'm moving all filing system access into a single library and eliminating fs-extra calls along the way.

1 Like