Ecmascript Modules are the future for packaging JS code, and both Node.js and Coffeescript support them. But I've had some trouble getting their support for ESM to work together.
The current stable Node (12.x) has ESM modules behind a flag (--experimental-modules). Coffeescript supports passing flags through to Node with --nodejs. So with a couple of files using ESM modules:
# a.coffee
import b from './b.coffee'
b()
# b.coffee
b = ->
console.log "Hello"
export default b
In theory, we can run this code with npx coffee --nodejs --experimental-modules a.coffee. In practice this raises an error:
13:24 $ npx coffee --nodejs --experimental-modules a.coffee
(node:8923) ExperimentalWarning: The ESM module loader is experimental.
(node:8923) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
/media/projects/coffeemodules/a.coffee:1
import b from './b.coffee';
^^^^^^
SyntaxError: Cannot use import statement outside a module
...
The error and docs say there are two ways to flag a file as containing an ESM module, one is to use the mjs extension (which isn't available to us here), and the other is to set "type": "module" in package.json, which also doesn't seem to work.
So: can it be done? Is there a way to get Coffeescript and Node.js ES modules to play together?
Related
es6 modules are supposed to work in coffeescript (see https://coffeescript.org/#modules), but even with an extremely simple project, it doesn't work for me. I:
Created a new directory
Ran 'npm init -y' in it
Added the key "type": "module" in my package.json
Created 2 files: index.coffee and utils.coffee
import {myprint} from 'utils.coffee'
myprint("Hello, World!")
export myprint = (str) ->
console.log(str)
when I try to execute index.coffee (via 'coffee index.coffee' - I've tried both git's bash shell - on Windows and PowerShell), I get the following error message:
(node:1856) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
and later:
SyntaxError: Cannot use import statement outside a module
This is all correct as far as I understand. Coffeescript transpiles the import/export statements correctly, but does not provide an environment to run them.
From the documentation:
Note that the CoffeeScript compiler does not resolve modules; writing an import or export statement in CoffeeScript will produce an import or export statement in the resulting output. It is your responsibility to transpile this ES2015 syntax into code that will work in your target runtimes.
You won't be able to run this code via either coffee or node --require coffeescript/register. I believe you will have to transpile your code and run the resulting JS via node
Before the actual questions (see at the end), please let me show the steps that lead to that question through an example:
Creating the project
tests$ mkdir esm && cd esm
tests/esm$ nvm -v
0.37.2
tests/esm$ nvm use v15
Now using node v15.6.0 (npm v7.5.6)
tests/esm$ node -v
v15.6.0
tests/esm$ npm -v
7.5.6
tests/esm$ npm init
package name: (esm) test-esm
entry point: (index.js)
Installing nodehun
tests/esm$ npm install nodehun
added 2 packages, and audited 3 packages in 11s
tests/esm$ npm ls
test-esm#1.0.0 tests/esm
└── nodehun#3.0.2
dependencies of nodehun here
index.js
import { suggest } from './checker.js'
suggest("misspeling");
checker.js
import Nodehun from 'nodehun'
import fs from 'fs';
const affix = fs.readFileSync('dictionaries/en_NZ.aff')
const dictionary = fs.readFileSync('dictionaries/en_NZ.dic')
const nodehun = new Nodehun(affix, dictionary)
export const suggest = (word) => hun_suggest(word);
async function hun_suggest(word) {
let suggestions = await nodehun.suggest(word);
console.log(suggestions);
}
To obtain the required Hunspell dictionary files (affix and dictionary):
tests/esm$ mkdir dictionaries && cd dictionaries
tests/esm/dictionaries$ curl https://www.softmaker.net/down/hunspell/softmaker-hunspell-english-nz-101.sox > en_NZ.sox
tests/esm/dictionaries$ unzip en_NZ.sox en_NZ.aff en_NZ.dic
Running the project
As per nodejs documentation (Determining Module System) to support the import / export:
Node.js will treat the following as ES modules when passed to node as the initial input, or when referenced by import statements within ES module code:
• Files ending in .js when the nearest parent package.json file contains a top-level "type" field with a value of "module".
We add "type": "module" field in the package.json file of the project.
package.json
{
...
"main": "index.js",
"type": "module",
...
}
First Failed Run
tests/esm$ node index.js
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".node" for tests/esm/node_modules/nodehun/build/Release/Nodehun.node
... omitted ...
at async link (node:internal/modules/esm/module_job:64:9) {
code: 'ERR_UNKNOWN_FILE_EXTENSION'
}
Digging a bit on the reason of the above error:
in the documentation on how to load addons, it refers to the use of require
The filename extension of the compiled addon binary is .node (as opposed to .dll or .so). The require() function is written to look for files with the .node file extension and initialize those as dynamically-linked libraries.
once you define your node project as a "type": "module", require it ceases to be supported (as specified in Interoperability with CommonJS):
Using require to load an ES module is not supported because ES modules have asynchronous execution. Instead, use import() to load an ES module from a CommonJS module.
Temporary Solution
After some time searching the documentation, I found a temporary solution: Customizing ESM specifier resolution algorithm:
The current specifier resolution does not support all default behavior of the CommonJS loader. One of the behavior differences is automatic resolution of file extensions and the ability to import directories that have an index file.
The --experimental-specifier-resolution=[mode] flag can be used to customize the extension resolution algorithm.
To enable the automatic extension resolution and importing from directories that include an index file use the node mode.
tests/esm$ node --experimental-specifier-resolution=node index.js
(node:XXXXX) ExperimentalWarning: The Node.js specifier resolution in ESM is experimental.
(Use `node --trace-warnings ...` to show where the warning was created)
[
'misspelling',
'misspending',
'misspeaking',
'misspell',
'dispelling',
'misapplier',
'respelling'
]
There are a some posts that get to this same resolution (ref 1, ref 2).
However, using experimental flags does not seem a proper way to run your application on production.
Failed Alternative with esm package
From that point, several failed attempts have been tried to avoid the use of --experimental-* flags. Doing some search, I found some posts (ref 1, ref 2) recommending the use of the esm package.
esm gets 1.3M downloads per week.
According the read-me file in GitHub, it does not require any changes.
However, at this point, when I try this node -r esm index.js, a new error appears:
tests/esm$ npm install esm
added 1 package, and audited 4 packages in 709ms
tests/esm$ npm ls
test-esm#0.1.0 tests/esm
├── esm#3.2.25
└── nodehun#3.0.2
tests/esm$ node -r esm index.js
tests/esm/index.js:1
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: tests/esm/index.js
at new NodeError (node:internal/errors:329:5)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1125:13) {
code: 'ERR_REQUIRE_ESM'
}
The above could be due to a reported issue (Error [ERR_REQUIRE_ESM]: Must use import to load ES Module / require() of ES modules is not supported).
There is proposed patch to fix it, although I do not know how to use it myself.
const module = require('module');
module.Module._extensions['.js'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
module._compile(content, filename);
};
Questions
Is there a (standard) way to use import / export (ES Modules) without incurring in issues with import addons?
Avoiding the use of the --experimental-specifier-resolution=node flag.
Perhaps esm could be the solution to the above. Is there anything I am doing wrong with the usage esm package?
If it is a correct usage, is there a way to use the proposed patch myself as a working around?
Any hints to help to solve it would be really appreciated.
Note: the final status of the example can be cloned from https://github.com/rellampec/test-esm.git
I ran into a similar problem and fixed it this way:
https://nodejs.org/api/module.html#module_module_createrequire_filename
// The project is "type": "module" in package json
// createRequire is native in node version >= 12
import { createRequire } from 'module';
import path from 'path';
// Absolute path to node modules (or native addons)
const modulesPath = path.resolve(process.cwd(), 'node_modules');
// Create the require method
const localRequire = createRequire(modulesPath);
// require the native add-on
const myNativeAddon = localRequire('my-native-addon');
After some ramblings trying to figure this out got to the root cause.
When using node -r esm index.js, the esm package does already all the work for your (as noted in other answers), and therefore (not mentioned in other answers):
the package.json should be updated by removing "type:" "module" (as it creates unintended interactions between the native node ES Modules feature and the esm package you installed)
Aside note: if you tried to use node ES Modules and then you try to switch to esm package, it is very easy to miss this point.
I have code which I like to use inside nodejs (14.4) and the browser. For this code to work inside nodejs I need named imports like
import {Vector3} from "three;
ES 6 modules in general are working fine with the following changes:
package.json:
"type": "module",
An launching nodejs with --experimental-specifier-resolution=node so I don't have to specify file extensions. But for named imports nodejs still prints out:
SyntaxError: The requested module 'three' is expected to be of type CommonJS, which does not support named exports.
There is a Stackoverflow post suggesting the usage of esm package loader. Unfortunately it has a bug making TypeScript "reflect-metadata" unusable (Issue: https://github.com/standard-things/esm/issues/809) So I can't use that.
TL;DR; How can I enable named ES 6 modules in nodeJs 14.4 without ESM package loader? type: module and launch arg are already set.
This may not be the perfect answer but I solved my problem by switching the code base to Type Script (TS):
For browser, I can configure ts to use es 6 modules
For node, I can configure ts to use node modules
--> Everybody is happy
I started seeing:
(node:6420) Warning: require() of ES modules is not supported.
when starting my webpack build and was wondering if using all import export for webpack.config.js was supported yet.
edit 1: I want to know if it's supported without using #babel/register or other transforms
Is is supported in Node 13. You can use either the .mjs extension (for files where you need to use import/export), or set "type": "module" in your package.json.
If your code runs in Node, you can use the fs package to interact with the file system
I'm new to nodejs.
I see we are using
const http = require('http')
instead of import . I found of that is because nodejs is older than es6
why node uses require not import?
However, I can use arrow functions. which is es6. how it is possible?
Thanks
It's finally happened: nearly 4 years after the import keyword was introduced in ES6, Node.js introduced experimental support for ES6 imports and exports. In Node.js 12, you can use import and export in your project if you do both of the below items.
1) Add the --experimental-modules flag when running Node.js
2) Use the .mjs extension or set "type": "module" in your package.json.
This package.json is very important. The type: "module" property tells Node.js to treat .js files as ESM modules. In other words, {"type":"module"} tells Node.js to expect import and export statements in .js files.
You can run file like this
node --experimental-modules index.js