头图
  • problem scenario
  • Before the npm package modification, only esm was supported
  • After the npm package is transformed, it supports both esm and cjs
  • Why do I still get an error after remodeling?
  • How to understand ts compile configuration esModuleInterop?
  • Summarize

problem scenario

Encountered a very interesting scene, cjs needs to introduce modules that were originally packaged in esm mode.

That is, you want to introduce an export module through require().

The exposure method of the my-npm-package package is:

 import foo from "./foo";
import bar from './bar';
export { foo, bar };

supported by

 import {foo, bar} from 'my-npm-package';

Packages in cjs that want to use the esm method

 const { foo } = require("my-npm-package");

An error will be reported: SyntaxError: Cannot use import statement outside a module

So how to make the package that only supports esm mode transform to support both esm and cjs?
The packaging method is commonjs.
This only supports cjs, how does esm support it?
Support for esm is supported by the babel transformation of the project importing the package.

Before the npm package transformation, only esm was supported

tsconfig.json

 {
  "compilerOptions": {
    "target": "ES2015",
    "module": "esnext",
  }
}

Packaging result:

 import foo from "./foo";
import bar from './bar';
export { foo, bar };
//# sourceMappingURL=index.js.map

After the npm package is transformed, it supports both esm and cjs

tsconfig.json

 {
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs"
  }
}

Packaging result:

 "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.bar = exports.foo = void 0;
const foo_1 = require("./foo");
exports.foo = foo_1.default;
const bar_1 = require("./bar");
exports.bar = bar_1.default;
//# sourceMappingURL=index.js.map

cjs:exports.xxx
esm: Object.defineProperty(exports, "__esModule", { value: true });

What is the reason for "csj to import the original way as esm package"?

 exports.xxx

What is the reason why the original esm package can still be used normally?

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

That is "__esModule", webpack will recognize the module as esm according to __esModule, and finally import it by converting it into a cjs module through babel.

Back to our scenario: what is the reason for transforming the esm module to support both cjs and esm?

The first step: target is changed from esm to commonjs to support cjs
Step 2: This step is actually unnecessary. The babel of the main project has been configured. All esm and cjs packages can be imported through esm.

Why do I still get an error after remodeling?

Let’s talk about the conclusion first: because the tsc cjs method is packaged, the package that imports a from 'a', a.method() will be converted into const a_1 = require('a'), a_1.default.method() by default. And some npm packages do not have exports.default.
How to fix: Enable esModuleInterop.

TypeError: Cannot read properties of undefined (reading 'stringify')

This is because, in our npm package, there is a dependency on query-string.

 import queryString from 'query-string';
 const query_string_1 = require("query-string");
 query_string_1.default.stringify(body) // 这里发生了报错

After being packaged by tsc, it will be converted to query_string_1.default.

But index.js of query-string@7.1.1 does not expose default.

after conversion

 const query_string_1 = exports;
 // query-string@7.1.1
exports.parseUrl
exports.stringifyUrl 
exports.pick
exports.exclude
exports.stringify
exports.extract
exports.parse

So how to solve this problem? Turn on esModuleInterop in tsconfig.json to true.
Thus returning exports as default.

 {
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "esModuleInterop": true
  }
}

Packaging result:

 // index.js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.bar = exports.foo = void 0;
const foo_1 = __importDefault(require("./foo"));
exports.foo = foo_1.default;
const bar_1 = __importDefault(require("./bar"));
exports.bar = bar_1.default;
//# sourceMappingURL=index.js.map

Not only index.js will inject __importDefault, but all ts files compiled by tsc will inject __importDefault.

 // foo.js
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
const query_string_1 = __importDefault(require("query-string"));

After __importDefault conversion, it becomes

 const query_string_1 = __importDefault( exports );

after conversion

 const query_string_1 = { default: exports };
 query_string_1.default.stringify(body) // 这里就没问题了。

How to understand ts compile configuration esModuleInterop?

In addition to the case where default is introduced by default, the case of importing by namespace also needs to be configured with esModuleInterop for compatibility.

Let's take a look at the official ts documentation: https://www.typescriptlang.org/tsconfig#esModuleInterop

By default, esModuleInterop is turned off, and ts is handled in the same way as CommonJS/AMD/UMD modules are handled as es6 modules. There are two cases where this cannot be handled:

  • ❌ import * as moment from "moment" as const moment = require("moment")
  • ❌import moment from "moment" as const moment = require("moment").default

After opening, these two problems can be avoided:

 import * as fs from "fs";
import _ from "lodash";
fs.readFileSync("file.txt", "utf8");
_.chunk(["a", "b", "c", "d"], 2);

When disabled (require directly):

 "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const lodash_1 = require("lodash");
fs.readFileSync("file.txt", "utf8");
lodash_1.default.chunk(["a", "b", "c", "d"], 2);

When enabled (auxiliary import functions __importStar, __importDefault):

 "use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const lodash_1 = __importDefault(require("lodash"));
fs.readFileSync("file.txt", "utf8");
lodash_1.default.chunk(["a", "b", "c", "d"], 2);

Let's take a look at the article of a front-end classmate on Zhihu: https://zhuanlan.zhihu.com/p/148081795

The core idea that esm can interop (interoperate) by introducing cjs is: esm has default, but cjs does not, add default to cjs module.

To quote the author, it is very concise:

At present, many commonly used packages are developed based on cjs / UMD, and writing front-end code is generally writing esm, so the common scenario is that esm imports the cjs library. However, due to the conceptual differences between esm and cjs, the biggest difference is that esm has the concept of default but cjs does not, so there will be problems with default. TS babel webpack has its own set of processing mechanisms to deal with this compatibility problem. The core idea is basically to add and read the default attribute.

Summarize

1. How to package esm modules as cjs?

module is changed to commonjs.

2. Why can esm reference the cjs package through import?

Babel will convert import to require.

3. How to understand esModuleInterop?

Compatible with packages that only have umd and cjs methods and do not expose the default attribute, add the default attribute, so that the package imported by import a from "a" or import * as a from "a" will not report that there is no default attribute. For example a package like query-string@7.1.1.
To be on the safe side, it is recommended to enable this configuration.

4. Why won't an error be reported when the module is esnext?

Because when the module is esnext, the code is directly in the esModule mode, that is, the import, default mode, and will not be converted to cjs with a default suffix.

It can be said, how to write, packaged is the original.

 import webcVCS from "./webcVCS";
import generateAssets from './generateAssets';
export { webcVCS, generateAssets, };
 import queryString from 'query-string';

5. After packaging, how to configure the module?

  • esnext: package only used in esm environment
  • commonjs: pure cjs or a package used in both cjs and esm environment (esm environment is generally supported by the project that installs the package, combined with webpack, babel and other packaging tools)
  • umd: same as commonjs, and need to support cjs, amd, cmd at the same time

趁你还年轻
4.1k 声望4.1k 粉丝