I have created some npm modules and compile them to:
commonJS (using exports.default =) and
esm (using export default)
I set up my package.json like so:
main: "index.cjs.js",
module: "index.esm.js"
When I npm install the package and I simple import it like:
import myPackage from 'my-package'
It will automatically choose the main file, not the module.
My question:
Is there a way to import the module file instead when doing import myPackage from 'my-package' in a JavaScript file?
Why I choose the commonJS file for "main":
I noticed that using Node, importing an esm file is not possible because of export default, it has to be commonJS. I have some simple helper JS functions like this and this, and I would want them to be usable to the widest audience. That's why I chose cjs for the "main" path in package.json.
Why I define a separate "module" in package.json:
Lots of famous libraries like Vue.js are already doing this. See further information on this Stackoverflow thread:
What is the "module" package.json field for?
Why do I want to be able to import the "module" file instead of the "main" file:
There is currently a bug in Rollup where it will not properly show JSDoc comments when coding after having imported a cjs file, but it does work when importing a es file.
The workaround I want to avoid:
Just set "main" to the esm file in package.json, right? But then all users who are using my packages in Node apps will not be able to use it anymore...
→ I'm really confused about all this as well, but I think I did enough research to make sense of all it. That being said, if anyone knows a better approach or any other advice, please do tell me in the comments down below!!
Just don't use extension for main file and have es6 and CommonJS version as two separate files with the same name and in the same directory, but with different extension, so:
index.js // transpiled CommonJS code for old nodejs
index.mjs // es6 module syntax
and in package.json:
{
"main": "index"
}
If node is launched with --experimental-modules flag, it would use *.mjs file, otherwise *.js.
Nodejs does not support "module" but does support the newer "exports" spec.
https://nodejs.org/api/packages.html#exports
https://github.com/nodejs/node/blob/v16.14.0/lib/internal/modules/esm/resolve.js#L910
"exports": {
"import": "./main-module.js",
"require": "./main-require.cjs"
},
Related
I am getting the following error when trying to run npm build on my serverless aws-nodejs-typescript project and do not understand how to fix it. Anyone able to point me in the right direction please?
npm build
(node:44390) UnhandledPromiseRejectionWarning: Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /usr/local/lib/node_modules/npm/node_modules/chalk/source/index.js
require() of ES modules is not supported.
require() of /usr/local/lib/node_modules/npm/node_modules/chalk/source/index.js from /usr/local/lib/node_modules/npm/lib/utils/explain-dep.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /usr/local/lib/node_modules/npm/node_modules/chalk/package.json.
...
Thanks very much!
The latest version of chalk has changed to ES6 import/export syntax instead of CJS (CommonJS with require). So instead of:
const chalk = require('chalk');
You have to do:
import chalk from 'chalk';
You'll (unfortunately) have to change all the other requires into imports as well, then change all your module.exports into export default and all your exports.whatever to export whatever; where whatever is your thing, or as suggested rename your file to index.cjs to force CJS.
Then there are the docs for import and export if you need them.
Then, you need to add type: "module" to package.json. So much for chalk :)
I didn't like that so I used to just use colors intead, but...
I uploaded my first module to npm in Javascript and I want it to support import and require. Is this possible?
The module is this:
https://www.npmjs.com/package/#dariodigulio/pro-log-js
Yes, I get the same error when I did try to require() your package. But, then I manged to get both working (import...from and require()) by converting your package code to commonjs compatible.
You should try to publish the package as commonjs (require()) module and then you'll get support from Node to choose either syntax on application side.
I did following change in your code to get both syntax working.
Removed the "type": "module" from package.json.
Update the main entry from "main": "ProLog.mjs", to "main": "ProLog.js".
Replace the import...from syntax with require() in ProLog.js file.
Update the export class ProLog to module.exports = ProLog.
I have a number of published npm packages that I have upgraded to provide both commonjs and esm builds. Some of the packages might be for both node and the browser. All packages compiled with webpack or rollup. All are written in typescript and transpiled into a dist directory.
I create a commonjs index.js file that looks like this:
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./react-abortable-fetch.cjs.production.min.js')
} else {
module.exports = require('./react-abortable-fetch.cjs.development.js')
}
I set the package.json main field to the above index.js file.
I also generate a .esm.js file for each package and I set both browser and module fields to the esm.js file and set the type file to be module.
The end result is something like this:
"type": "module",
"main": "dist/index.js",
"browser": "dist/react-abortable-fetch.esm.js",
"module": "dist/react-abortable-fetch.esm.js",
"types": "dist/index.d.ts",
The problem with this approach is that only esm packages can consume it (unless I am wrong).
What is the best way to configure the package.json file so that packages that have not made the leap yet (and that is quite a few) can still consume the package?
The idea is to leverage Node.js conditional exports to customize the import behavior depending on how you import the module (require or import).
In order to have two different interfaces for CommonJS and ESM, you could either:
Transpile to both CommonJS and ESM (allows to accidentally use both import and require for your library in the same application which could cause unwanted and unpredictable behaviors)
Have the implementation as ESM and a CommonJS wrapper (not possible because you can't require an ECMAScript module when it has top-level await)
Have the implementation as CommonJS and an ESM wrapper (best actual solution)
So you have to set CommonJS as a TypeScript transpilation target, then create an ESM wrapper (preferably in a dedicated folder). Finally, map the CommonJS and the ESM entry points to the corresponding files in the package.json:
"exports": {
"require": "./index.js",
"import": "./esm/index.js"
}
I have published a minimal working project here: https://github.com/Guerric-P/demo-commonjs-esm
In order to use the libraries in TypeScript, you also need one .d.ts file aside every JavaScript file.
So, I'm dabbling a bit with Typescript and Grunt at the moment to see if it's worth it for me. The thing is: Typescript does not compile to *.mjs files but only regular *.js files. Node does support ES6 Modules but only if you either mark them as '*.jsm' files or by setting "type": "module". Setting this top-level field in package.json however has global scope for any *.js file in the same directory and any following ones.
This breaks the Gruntfile.js file as it seems since it uses CommonJS modules, see my very basic Gruntfile as example:
module.exports = function (grunt) {
grunt.initConfig({
ts: {
default: {tsconfig: "./tsconfig.json"}
}
})
grunt.loadNpmTasks("grunt-ts");
grunt.registerTask("default", ["ts"]);
}
Without expecting much success I naively changed the export syntax from module.exports = to export default which expectedly did no work since it didn't make much sense.
Questions
Is there any option to use Grunt with ES6 modules enabled in node?
Is there a proper way to tell TypeScript to compile to *.mjs files?
If you set "type": "module" in your package.json, you need to rename Gruntfile.js to Gruntfile.cjs, and run it with grunt --gruntfile Gruntfile.cjs.
The suggested approach with Babel running before Grunt makes Grunt a bit redundant. Since TypeScript does not yet support exporting ES6 modules to *.mjs files (and you have to use the *.mjs suffix in your import when node should still be running with its CommonJS system) and will probably never fully (see Design Meeting Notes 11/22/2019) I have to conclude that ES6 modules still have serious implications and issues. Changing the file extension is not enough since the extension-less imports fail with node. You'd need to go through every compiled file and change the import to specifically load *.mjs files.
However, the TypeScript Compiler can be set up in a way that it does understand ES6 module syntax and to compile to CommonJS (see TS handbook).
{
"compilerOptions": {
"module": "CommonJS",
// [...]
},
}
This way the TypeScript code be written with ES6 module syntax and the output can be CommonJS compatible without braking other code. As a bonus you can skip the Babel approach and grunt can run TS compiler.
You can using babel-node to compile first. Which will resolve ES6 export and import problem.
npm install --save-dev #babel/core #babel/node
npx babel-node server.js
I'm creating a command-line script, using classes from the main express app.
Script resides in the folder:
bin/utils/
├── sync-buyers.ts
└── tsconfig.json
The main express app is in /app use uses import '#/foo/bar/thing.
This is set up in the tsconfig.json of the main app, as follows:
"paths": {
"#/*": ["*"],
"*": [
"node_modules/*",
"app/typings/*"
]
}
},
"include": ["app/**/*", "test/**/*"],
"exclude": ["app/**/*.test.ts", "/__tests__/", "/__mocks__/", "/__snapshots__/", "app/**/__mocks__/"],
"files": ["typings/global.d.ts"]
Script Execution
I'm testing to see if I can import from the main app, so I created a sayHello() function.
#!/usr/bin/env ts-node
/* tslint:disable */
import { sayHello } from '../../app/services/v2/oapp';
sayHello();
When I run it:
TSError: ⨯ Unable to compile TypeScript:
../../app/services/v2/oapp.ts(9,19): error TS2307: Cannot find module
'#/helpers/fetch'.
../../app/services/v2/oapp.ts(10,31): error TS2307: Cannot find module
'#/services/v2/buyer'.
../../app/services/v2/oapp.ts(11,51): error TS2307: Cannot find module
'#/http/HttpHeader'.
Summary:
Does ts-node support '#' style of import? If so, how do I set it up?
So the TypeScript paths configuration only applies to TypeScript's type resolution and checking, meaning that it will allow TypeScript to understand those imports for the purposes of type-checking only, but the code it generates won't automatically rewrite those imports to the correct locations.
There's two common approaches for solving this:
Update the Node resolver to understand the TypeScript paths config. The generated files will still refer to those paths by their #-name.
Most commonly, the tsconfig-paths module is used for this. You can require that module from the node command directly:
node -r tsconfig-paths/register main.js
Rewrite the generated files so that the #-names get replaced with the "real" local relative path locations.
There's a standalone module for this, tspath - you simply run tspath after compiling your TypeScript, and it updates the generated files with the correct paths.
If you're using Webpack, you can also use tsconfig-paths-webpack-plugin, which will take care of configuring Webpack's resolver to correctly locate those #-name paths.
And finally if you're using Babel, you might be interested in babel-plugin-module-resolver which does a similar thing for the Babel toolchain, however the downside here is it doesn't read the paths config from tsconfig.json, so you essentially have to duplicate your paths config in the alias config of this plugin.
Personally I'd recommend tsconfig-paths if this is a Node script or server that's compiled with tsc directly and tsconfig-paths-webpack-plugin if this is a frontend Webpack build.