Support modern ESM imports in Node for Max

attila's icon

Hi all, I'm working on a project involving Node for Max and struggling to get a basic non-trivial patch running.

Correct me if I'm wrong, but even with a custom @node_bin_path Node binary specified it seems like the node.script node process manager object in Max is hardcoded to import user scripts using the outdated CommonJS `require('xyz')` syntax.

In `/Applications/Max.app/Contents/Resources/C74/packages/Node for Max/source/lib/nsRunner.js`:

// Kick off things and load user script
require(process.env.SCRIPT_PATH);
loadedSuccessfully = true;

The README states that Node v16.6.0 is used internally. In Node.js v12.17.0 and above it's possible to use dynamic imports. A compromise could be:

// if package.json nearest to the user script has type: 'module'
// or the script file ends with .mjs
import(process.env.SCRIPT_PATH).then(() => {
loadedSuccessfully = true;
})

or something less hacky.

These days, a number of package dependencies are being published in ESM-only formats. The broader implication is that it's not always feasible to convert an entire script to CommonJS. A modern Node project often comes with additional complexity, especially if there's Typescript transpilation or bundling involved. Often there are packages involved with add-on Node-API binaries (`.node`) that cannot be bundled and need to be marked as runtime externals (in my current case, one that's adding WebRTC support for Node).

Is there a currently preferred workaround or consideration for this? It seems like I would have to resort to some kind of a Max for Node proxy to exchange messages with a locally running, completely separate node process.

attila's icon

To save some hours in case anyone finds this thread — I was able to get a typescript + ESM project running in Max via esbuild bundling to commonjs and an unexpected hack to mix import/require. The latter is required for interaction with the Max API.

// build script
esbuild index.ts --platform=node --bundle '--external:max-api' --format=cjs --outfile=./index.js

// index.ts
import esmOnlyDefault from 'esm-only-package'
import commonJsDefaultViaEsm from 'cjs-only-package' // ES Modules imports are static
const maxAPI = require('max-api') // CommonJS imports are dynamically resolved at runtime

loadmess's icon

Hi!
Thank you digging and sharing this solution with the community.
I'm interested in this but I confess I don't have enough flexibility to understand your approach.
Could you possibly add more information on how to implement this?
I'm trying to test this library:
https://www.npmjs.com/package/hdsp2

Thank you

Florian Demmer's icon

Thanks for pointing this out and providing a workaround. It seems to in fact point towards a limitation in the way we are currently loading the process shell on the Node / JS side.

I’ve filed a ticket and we’ll be looking into this.

Thanks
Florian

Misemao's icon

Is the best way to get notified about the state of the support of modern esm imports subscribing to this thread?
I have a bunch of projects that I could finally integrate fully in max without having to use external scripts sending OSC to max once esm imports are possible.

josh ball's icon

Also very interested in this functionality if it hasn't already been handled.