Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESM module import requires using .default property #1381

Closed
maikknebel opened this issue Jan 5, 2021 · 16 comments · Fixed by #1757
Closed

ESM module import requires using .default property #1381

maikknebel opened this issue Jan 5, 2021 · 16 comments · Fixed by #1757
Assignees
Milestone

Comments

@maikknebel
Copy link

The version of Ajv you are using:
v7.0.3

Operating system and node.js version
Win10, node 15.5.1

Package manager and its version
npm v6.14.10; yarn v1.22.5

Link to (or contents of) package.json

{
  "name": "ajv-test",
  "version": "1.0.0",
  "description": "",
  "main": "index.mjs",
  "scripts": {
    "app": "node index.mjs"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "ajv": "^7.0.3"
  }
}
// file: index.mjs
import Ajv from 'ajv';

const ajvValidator = new Ajv();

Error messages

npm run app

> ajv-test@1.0.0 app C:\Users\maik\Projekte\ajv-test
> node index.mjs

file:///C:/Users/maik/Projekte/ajv-test/index.mjs:3
const ajvValidator = new Ajv();
                     ^

TypeError: Ajv is not a constructor
    at file:///C:/Users/maik/Projekte/ajv-test/index.mjs:3:22
    at ModuleJob.run (node:internal/modules/esm/module_job:152:23)
    at async Loader.import (node:internal/modules/esm/loader:166:24)
    at async Object.loadESM (node:internal/process/esm_loader:68:5)

The output of npm ls

ajv-test@1.0.0 C:\Users\M.Knebel\Projekte\ajv-test
`-- ajv@7.0.3
  +-- fast-deep-equal@3.1.3
  +-- json-schema-traverse@1.0.0
  +-- require-from-string@2.0.2
  `-- uri-js@4.4.0
    `-- punycode@2.1.1

When i downgrade to v6.x.x everything works fine. I also tried yarn for package installation, deleted node_modules and setup a new project to avoid conflicts from other code.

@epoberezkin
Copy link
Member

It is not tested with .mjs...

It works if you use it like that:

// index.mjs
import Ajv from "ajv"
const ajv = new Ajv.default()

I thought that it would understand default exports, it probably needs some additional typescript setting to support import from .mjs

@epoberezkin
Copy link
Member

possibly you need to recompile source to .mjs?

@maikknebel
Copy link
Author

maikknebel commented Jan 7, 2021

.mjs makes nodejs treat the file as ECMAScript-Module, and makes me able to use import / export syntax instead of require(), no need to compile npm-packages to be compatible. import syntax works with non module-packages.

Now that you write it, i checked the package and since in dist/ajv.js uses exports.default = Ajv;, it is exposing the Ajv-Class with the name 'default'. Is this intended?

After checking other installed npm packages in my projects, i see only default exports using module.exports = ... or export default .... The first is working with both import and require. Maybe this should be adapted to this package.

new Ajv.default() solves the problem, looks a bit wired/unfamilary but as long as it works.

The following code-change made it work for me with new Ajv();:

// file: node_modules/ajv/dist/ajv.js
...
exports.default = Ajv;
module.exports = Ajv; // i added this line

@noelleleigh
Copy link

I'm running into a similar issue using Rollup to bundle Ajv into a bundle for the browser.

package.json

{
  "name": "avj-test",
  "version": "1.0.0",
  "dependencies": {
    "ajv": "^7.0.3"
  },
  "devDependencies": {
    "@rollup/plugin-node-resolve": "^11.0.1",
    "rollup": "^2.36.1"
  }
}

rollup.config.js

import { nodeResolve } from "@rollup/plugin-node-resolve";

export default {
  input: 'src/index.js',
  output: {
    dir: 'output',
    format: 'iife',
  },
  plugins: [nodeResolve()],
};

src/index.js

import Ajv from "ajv"

const ajv = new Ajv()

Output

% npx rollup --config
src/index.js → output...
[!] Error: 'default' is not exported by node_modules/ajv/dist/ajv.js, imported by src/index.js
https://rollupjs.org/guide/en/#error-name-is-not-exported-by-module
src/index.js (1:7)
1: import Ajv from "ajv"
          ^
2: 
3: const ajv = new Ajv()
Error: 'default' is not exported by node_modules/ajv/dist/ajv.js, imported by src/index.js
    at error (/Users/noelle/dev/avj-test/node_modules/rollup/dist/shared/rollup.js:5265:30)
    at Module.error (/Users/noelle/dev/avj-test/node_modules/rollup/dist/shared/rollup.js:9858:16)
    at handleMissingExport (/Users/noelle/dev/avj-test/node_modules/rollup/dist/shared/rollup.js:9747:28)
    at Module.traceVariable (/Users/noelle/dev/avj-test/node_modules/rollup/dist/shared/rollup.js:10251:24)
    at ModuleScope.findVariable (/Users/noelle/dev/avj-test/node_modules/rollup/dist/shared/rollup.js:8770:39)
    at Identifier$1.bind (/Users/noelle/dev/avj-test/node_modules/rollup/dist/shared/rollup.js:4138:40)
    at NewExpression.bind (/Users/noelle/dev/avj-test/node_modules/rollup/dist/shared/rollup.js:2868:23)
    at NewExpression.bind (/Users/noelle/dev/avj-test/node_modules/rollup/dist/shared/rollup.js:8035:15)
    at VariableDeclarator.bind (/Users/noelle/dev/avj-test/node_modules/rollup/dist/shared/rollup.js:2868:23)
    at VariableDeclaration.bind (/Users/noelle/dev/avj-test/node_modules/rollup/dist/shared/rollup.js:2864:31)

The issue is fixed with this change:

node_modules/ajv/dist/ajv.js

  exports.default = Ajv;
+ export default Ajv
  //# sourceMappingURL=ajv.js.map

@epoberezkin
Copy link
Member

epoberezkin commented Jan 8, 2021

The problem with the below

module.exports = Ajv

is that you cannot export anything else from this module other than this thing.

So I won't be able to do in typescript something like import Ajv, {_} from "ajv"

The problem with this:

export default Ajv

is that I think it is not a valid JS outside of .mjs, plus it has to be compiled by typescript compiler, rather than just added.

I didn't figure out a better way to compile typescript in such a way that I can both have default export compatible with typescript and with require, and also be able to export other things from the same file - hence this compromise that allows above import in typescript but requires .default both with require and, as it turns out, with import from .mjs

There may be a better, more universal solution probably, in configuring typescript options in such way that at least a normal import (without .default) from .mjs works. Possibly .mjs file has to be compiled too, so the import from .mjs would prefer .mjs file.

Any ideas on how best to configure typescript (while still allowing the above imports and without using esModuleInterop setting) are welcome

@epoberezkin epoberezkin changed the title ESM module import creates an error in nodeJs ESM module import requires using .default property Feb 7, 2021
@teppeis
Copy link

teppeis commented Feb 17, 2021

Unfortunately, TypeScript does not yet support Node.js native ESM.
In particular, default export with module="CommonJS" is incompatible.
It doesn't allow you to output .mjs, nor does it provide effective options.

One hacky workaround to support native ESM is to add module.exports = Ajv after export default class Ajv {}.

export default class Ajv extends AjvCore {
// ...
}

module.exports = Ajv;
module.exports.default = Ajv;
Object.defineProperty(exports, "__esModule", { value: true });

You will also need to move all exports to after the part where you added them.

This technique is used in some packages, such as got.
https://github.com/sindresorhus/got/blob/d0cd709fdd61d4e5762866309ede1b25cdbe8e60/source/index.ts#L128-L133
The output
https://npmfs.com/package/got/11.8.1/dist/source/index.js

@teppeis
Copy link

teppeis commented Feb 17, 2021

@epoberezkin
This is an example of a change.
teppeis@7e0a836

If necessary, the other core.ts, 2019.ts and jtd.ts need to be changed as well.
If you agree with this policy, I will send you a PR.

@epoberezkin
Copy link
Member

epoberezkin commented Mar 4, 2021

@teppeis thank you - maybe not such a terrible idea...

So in this case all the extra bits will be added as properties of the constructor function, but probably no harm.

Also requirement to add .default require would go away (but would still work, so no backwards compatibility issue, can be probably released without waiting for v8).

It does look like the linked example is compiled in esModuleInterop mode, but it should work without it as well. I am not sure whether Object.defineProperty(exports, "__esModule", { value: true }); is needed - it looks like the referenced typescript bug is fixed now, so we probably don't need it?

@epoberezkin
Copy link
Member

I would not bother fixing it in core.ts, I don't think anybody would import it directly, but the user facing classes can be updated indeed - PR would be great!

@FloEdelmann
Copy link

FloEdelmann commented Mar 7, 2021

Another option would be to export the Ajv class as a named export in addition to the default export:

export class Ajv {}

export default Ajv

Example usage:

const Ajv = require('ajv').default
const {Ajv} = require('ajv')
import {Ajv} from 'ajv'

But that would not enable the default use case:

const Ajv = require('ajv') // wrong
import Ajv from 'ajv' // wrong

@Nowadays
Copy link

Nowadays commented Mar 10, 2021

Hi @epoberezkin , got into this issue as well which made our team switch to Joi for validation because it conflicts with the fact that Jest requires transpiling into commonjs, which creates issues, see: svsool/axios-better-stacktrace#3 for example.

All the above solutions are nice but require editing the source code, when a nicer solution would be to compile for both ESM and CJS, then create a sub-package.json in each output folder where:

cjs/package.json:

{
  "type": "commonjs"
}

esm/package.json:

{
  "type": "module"
}

and in the main package.json:

"exports": {
    ".": {
        "import": "./esm/index.js",
        "require": "./cjs/index.js"
    }
}

I can make a PR for this if it's accepted.

@epoberezkin epoberezkin self-assigned this Mar 13, 2021
@epoberezkin epoberezkin added this to the 8.0.0 milestone Mar 14, 2021
@epoberezkin
Copy link
Member

@Nowadays Node.js marks exports syntax as "experimental" I am not very keen to support it until it stabilises (by which time only module syntax may need to be supported, rather than both), particularly given that there are multiple ways to support it from import sites.

I'm going to add what @teppeis proposed:

export default class Ajv extends AjvCore {
// ...
}

module.exports = Ajv
module.exports.default = Ajv

@epoberezkin
Copy link
Member

So, module.exports = Ajv and moving exports below is not enough - export statements still assign to exports so they are not exported because module.exports is different now.

What seems to work is module.exports = exports = Ajv, as long as all other exports are below.
So the "hack" should be:

class Ajv extends AjvCore {
// ...
}

module.exports = exports = Ajv
exports.__esModule = true // this is still needed because "exports" is overridden (and TS compiler does it in the beginning)

export default Ajv

@bhvngt
Copy link
Contributor

bhvngt commented Aug 27, 2021

I am facing similar issue while importing ajv/dist/standalone/index.js. Here's sandbox that reproduces the issue.

Should we apply similar hack for standalone module?

@SebVory
Copy link

SebVory commented Mar 14, 2022

I updated Node.js version with npm version to

npm -v
8.3.1
node -v
v16.14.0

and I am unable to npm run build or npm start, here is the error I am getting

/Users/lordsebastian/Work/timeisltd/orgchart/web/node_modules/mini-css-extract-plugin/node_modules/schema-utils/dist/validate.js:66
const ajv = new Ajv({
            ^
TypeError: Ajv is not a constructor
    at Object.<anonymous> (/Users/lordsebastian/Work/timeisltd/orgchart/web/node_modules/mini-css-extract-plugin/node_modules/schema-utils/dist/validate.js:66:13)
    at Module._compile (node:internal/modules/cjs/loader:1103:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1155:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/Users/lordsebastian/Work/timeisltd/orgchart/web/node_modules/mini-css-extract-plugin/node_modules/schema-utils/dist/index.js:6:5)
    at Module._compile (node:internal/modules/cjs/loader:1103:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1155:10)

I tried lot of things like deleting node_modules, cache, solving it via resolutions of new / old versions of mini-css-extract-plugin but I met no success. Can someone give me please an advice how to solve this?

@prewk
Copy link

prewk commented Jul 6, 2023

Not sure what's going on but I installed latest ajv in my frontend TS project (Angular, using Node 18):

Error: node_modules/ajv/lib/ajv.ts:34:1 - error TS2591: Cannot find name 'module'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

34 module.exports = exports = Ajv
   ~~~~~~


Error: node_modules/ajv/lib/ajv.ts:34:18 - error TS2304: Cannot find name 'exports'.

34 module.exports = exports = Ajv
                    ~~~~~~~


Error: node_modules/ajv/lib/ajv.ts:35:23 - error TS2304: Cannot find name 'exports'.

35 Object.defineProperty(exports, "__esModule", {value: true})
                         ~~~~~~~

I've tried enabling esModuleInterop to no avail. (Oh, and I already have the node types installed, not sure why I'd need them tho)

edit: Oh, sorry. It was this line that caused the trouble, which I of course can live without:

import { ErrorObject } from 'ajv/lib/types';

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

9 participants