I was playing around with adding esm to ember-cli and ran into an issue: it doesn't seem I can simply require mjs modules w/o adding default property:
// converted module
// utilities/require-as-hash.js
import path from 'path';
export default requireAsHash;
function requireAsHash() {
// code
}
// usage in test
const requireAsHash = require('../../lib/utilities/require-as-hash').default;
I did add esm option to package.json:
{
"esm": {
"cjs": true
}
}
Is there something I'm missing? I found this issue which I believe is the same but not sure if there was a resolution.
Hi @twokul!
You have an ES module named require-as-hash.js that has a default export and then when you require the ES module you want the default export to be treated like module.exports of CJS?
When we go from ESM to CJS we keep the exports explicit so folks can choose which ES export they'd like to access (so a named export or a default export). If you're able to require the ES module that means you're running within the esm loader so you should be able to static
import requireAsHash from "../../lib/utilities/require-as-hash" too?
You have an ES module named require-as-hash.js that has a default export and then when you require the ES module you want the default export to be treated like module.exports of CJS?
right. the reason we want to be able to do that is that we have modules that are our public api:
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
so if we were to convert ember-app to esm, it would break consumers b/c they would have to change their code to:
const EmberApp = require('ember-cli/lib/broccoli/ember-app').default;
Ah, okay! If you have folks consuming your API publicly then your entry point for ember-app should be a CJS bridge since the consumer may not be using esm in their own projects to load ember-app.
In the following example ember-app is a directory with an index.js and main.js file
require = require('esm')(module)
module.exports = require('./main.js').default
/* the ember-app ES module */
Then when user code loads
const EmberApp = require('ember-cli/lib/broccoli/ember-app')
the ESM code will pass over the CJS bridge to allow regular Node 6+ CJS consumers to load your ESM code.
@jdalton What if we need want to require an ES6 module from the current package? But still be able to import it elsewhere in ES6 code? Where should we put the bridge?
@samselikoff
The bridge allows loading ES modules and exporting them as CJS. The loaded ES modules can load other ES modules or CJS modules.
If the bridge is something like
require = require('esm')(module)
module.exports = require('./main.js')
Then other packages with esm enabled will load it as ESM without having to side-step the bridge or if they use babel the bridge will be seen as ESM as well since it exports the __esModule interop property.
Related to https://github.com/ember-cli/ember-cli/pull/7897.
Right – I guess my question is, within a single module, can I share an ES module with both CJS modules as well as other ES modules, and avoid the .default property thing?
I suppose the idea is once you use esm, you can convert all your modules over to ES...
I suppose the idea is once you use esm, you can convert all your modules over to ES...
Right, if they're being loaded from the esm loader. However, you're hitting on a transition case that I'd like to dig into more (it's coverable since it's also within the loader).
Using Babel as an example
export default "a"
is converted to
exports.__esModule = true;
exports.default = "a";
so then the CJS parent require-ing the module would also have to access .default.
However, if the file parent used static import (by way of Babel) it would resolve as
import a from "./a"
without a dangling .default.
So in Babel 6 this leaves the dangling .default when consumed by plain CJS require.
However, I too did not like this and used a plugin, babel-plugin-add-module-exports, to fix this. The plugin follows the babel@5 behavior if only the export default declaration exists. I can totally add something to esm to enable this as well. I'll need to think about the option a bit but it is trivial to implement for sure.
Update:
@twokul Bikeshed esm options name time:
options.cjs.addModuleExports
options.cjs.exportDefaultOnly
options.cjs.exportDefault
options.cjs.moduleExportsAsDefault
I'm open to other suggestions too.
@jdalton both options.cjs.addModuleExports and options.cjs.moduleExportsAsDefault sounds good to me; I'd personally go w/ `options.cjs.moduleExportsAsDefault, it's longer but explains what happens clearly :)
@twokul I'm leaning the options.cjs.moduleExportsAsDefault name too. I'll work on proofing out the implementation of this option this evening.
👍 moduleExportsAsDefault seems good to me also and will help in a number of cases
It'd also be useful for me to be able to disable the generation of dangling default prop on the export.
I have a situation where I've enabled esm for ava, and I've got the following snapshot test for my module (which is a sharable ESLint config, the source of which is in esm instead of the usual cjs because of consistency reasons in a monorepo):
import test from 'ava';
import eslintFindRules from 'eslint-find-rules';
test('Unused eslint rules', t => {
const unusedRules = eslintFindRules('src/index.js').getUnusedRules();
t.snapshot(unusedRules);
});
eslint-find-rules is able to load the file correctly after enabling esm, but it tries to validate the exported object and errors on the default property:
yarn run v1.9.4
$ ava
1 test failed
Unused eslint rules
/home/zeorin/code/poise/node_modules/eslint/lib/config/config-validator.js:235
Error thrown in test:
Error {
message: `ESLint configuration in /home/zeorin/code/poise/packages/eslint-config/src/index.js is invalid:␊
- Unexpected top-level property "default".␊
`,
}
validateConfigSchema (/home/zeorin/code/poise/node_modules/eslint/lib/config/config-validator.js:235:15)
Object.validate (/home/zeorin/code/poise/node_modules/eslint/lib/config/config-validator.js:261:5)
loadFromDisk (/home/zeorin/code/poise/node_modules/eslint/lib/config/config-file.js:521:19)
Object.load (/home/zeorin/code/poise/node_modules/eslint/lib/config/config-file.js:564:20)
Config.loadSpecificConfig (/home/zeorin/code/poise/node_modules/eslint/lib/config.js:135:46)
new Config (/home/zeorin/code/poise/node_modules/eslint/lib/config.js:101:14)
new CLIEngine (/home/zeorin/code/poise/node_modules/eslint/lib/cli-engine.js:420:23)
_getConfig (/home/zeorin/code/poise/node_modules/eslint-find-rules/dist/lib/rule-finder.js:24:19)
new RuleFinder (/home/zeorin/code/poise/node_modules/eslint-find-rules/dist/lib/rule-finder.js:98:16)
module.exports (/home/zeorin/code/poise/node_modules/eslint-find-rules/dist/lib/rule-finder.js:151:10)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
I'm working around this for the moment by testing a cjs build of the module (generated by Rollup) instead, but this has limitations with watch modes, and requires a build before the test can be run, which is a perf niggle.
Removing the dangling default would solve this problem.
Most helpful comment
@twokul I'm leaning the
options.cjs.moduleExportsAsDefaultname too. I'll work on proofing out the implementation of this option this evening.