Attempt to import native ES modules in Meteor 3.0-beta.0 (no luck yet)

I am trying hard to use vanilla ES modules (apparently not hard enough, I haven’t made changes to Meteor directly yet).

I have this .meteor/packages to avoid modules and ecmascript (there is much less need for those today, and most likely you only really need them if you’re using the new stage 3 decorators):

meteor-base@1.5.2-beta300.0             # Packages every Meteor app needs to have
mobile-experience@1.1.1-beta300.0       # Packages for a great mobile UX
mongo@2.0.0-beta300.0                   # The database Meteor supports right now
reactive-var@1.0.13-beta300.0            # Reactive variable for tracker

# commented stuff out, starting simple, avoiding problems
#standard-minifier-css@1.9.2   # CSS minifier run for production mode
#standard-minifier-js@2.8.1    # JS minifier run for production mode
#es5-shim@4.8.0                # ECMAScript 5 compatibility for older browsers
#ecmascript@0.16.7              # Enable ECMAScript2015+ syntax in app code
#typescript@4.9.4              # Enable TypeScript syntax in .ts and .tsx modules

shell-server@0.6.0-beta300.0            # Server-side component of the `meteor shell` command
hot-module-replacement@0.5.4-beta300.0  # Update client in development without reloading the page

static-html@1.3.3-beta300.0             # Define static page content in .html files


#accounts-ui # not supported in 3.0 yet, fails with constraint error
#accounts-password

session

and the release is METEOR@3.0-beta.0.

Now, for server code, placing "type": "module" in package.json will break Meteor 3.0:

So I tried to work around that, by setting package.json back to "type":"commonjs", defining meteor.mainModule,

	"meteor": {
		"mainModule": {
			"client": "client/entry.js",
			"server": "server/entry.js"
		}
	}

and then using eval to force Meteor to skip handling of import() syntax (why is it handling my import() syntax when I removed ecmascript and typesceript packages anyway?):

//server/entry.js
function nativeImport(path) {
	console.log('eval import')
	return eval(`import('${path}')`)
}

nativeImport('./imports/test.js').then(({foo}) => console.log('foo', foo))
//./imports/test.js
export const foo = 123

This results in Node.js telling me that import() is not available inside of a vm:

I20240106-01:09:39.825(-8)? eval import
W20240106-01:09:39.825(-8)? (STDERR) node:internal/errors:497
W20240106-01:09:39.825(-8)? (STDERR)     ErrorCaptureStackTrace(err);
W20240106-01:09:39.825(-8)? (STDERR)     ^
W20240106-01:09:39.825(-8)? (STDERR) 
W20240106-01:09:39.825(-8)? (STDERR) TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING]: A dynamic import callback was not specified.
W20240106-01:09:39.825(-8)? (STDERR)     at new NodeError (node:internal/errors:406:5)
W20240106-01:09:39.825(-8)? (STDERR)     at importModuleDynamicallyCallback (node:internal/modules/esm/utils:144:9)
W20240106-01:09:39.825(-8)? (STDERR)     at eval (eval at nativeImport (app/server/entry.js:21:12), <anonymous>:1:1)
W20240106-01:09:39.825(-8)? (STDERR)     at nativeImport (app/server/entry.js:21:12)
W20240106-01:09:39.825(-8)? (STDERR)     at app/server/entry.js:23:1
W20240106-01:09:39.825(-8)? (STDERR)     at app/server/entry.js:52:4
W20240106-01:09:39.825(-8)? (STDERR)     at load (packages/core-runtime.js:139:16)
W20240106-01:09:39.825(-8)? (STDERR)     at onDepLoaded (packages/core-runtime.js:118:7)
W20240106-01:09:39.825(-8)? (STDERR)     at packages/core-runtime.js:153:7
W20240106-01:09:39.825(-8)? (STDERR)     at Array.forEach (<anonymous>)
W20240106-01:09:39.825(-8)? (STDERR)     at packages/core-runtime.js:152:22
W20240106-01:09:39.825(-8)? (STDERR)     at evaluateNextModule (packages/core-runtime.js:179:14)
W20240106-01:09:39.825(-8)? (STDERR)     at packages/core-runtime.js:194:9 {
W20240106-01:09:39.825(-8)? (STDERR)   code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING'
W20240106-01:09:39.825(-8)? (STDERR) }
W20240106-01:09:39.825(-8)? (STDERR) 
W20240106-01:09:39.825(-8)? (STDERR) Node.js v20.9.0

Now I’m wondering what hoops I need to jump through next to get this code to not run in a vm, but as regular Node.js code.

I tried passing the --experimental-vm-modules option for Node, like this,

SERVER_NODE_OPTIONS=--experimental-vm-modules meteor --exclude-archs web.browser.legacy

but that didn’t do anything.

I’m guessing that at this point I would need to modify Meteor directly to achieve running native ES modules.

I really hope this can be a goal for 3.x because it is a major area in which Meteor is still lagging behind for a long time already.

3 Likes

Actually that nativeImport/eval wrapper is not needed if ecmascript is removed, and using import() directly will properly call Node’s import function, but it will have the same ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING error.

How can we make this code run with support for native import()? Is there a way to configure underlying Node to support import() in the vm without hacking Meteor directly?


Even if, for now, Meteor has to stay with "type": "commonjs" in projects, supporting calling of import() from the server entry will be an escape hatch to be able to load the rest of the app as native ES modules, from a sub-folder that has a nested package.json file with "type": "module" in it.

If we can simply enable import() inside the vm, this will simply work.


Then later, we can convert Meteor code to support "type": "module" at the top level.

1 Like

The really nice thing about getting this work

is going to be that we are going to be past the painful ES6 migration that caused all this tooling that was required for using the new syntax before engines supported modules.

Once we’re on native ES modules, it will be smooth sailing for a really long time. Only the death of JavaScript will be able to stop the sail boat, and obviously that’s not happening any time soon, maybe not even in our web-dev life times.

3 Likes