tsc, babel and webpack processing of module import and export

Problem introduction

Many react users may encounter such a problem when migrating from JS to TS:

JS introduces react as follows:

// js
import React from 'react'

TS is like this:

// ts
import * as React from 'react'

If you directly change the writing method to JS in TS, when @ types/react is installed, the editor will throw an error: This module is declared with "export =" and can only be used with the default import when using the "esModuleInterop" flag.

According to the prompt, in tsconfig Set compileroptions. JSON Esmoduleinterop is true, and the error message disappears.

To understand the cause of this problem, we first need to know the module system of JS. There are three commonly used JS module systems:

  • CommonJS (hereinafter referred to as cjs)
  • ES module (hereinafter referred to as esm)
  • UMD

(AMD uses less now, so it is ignored)

Compilers such as babel and TS prefer cjs. By default, the esm written in the code will be converted into cjs by babel and ts. For this reason, I speculate the following:

  1. cjs appeared earlier than esm, so a large number of npm libraries are based on cjs (much higher than esm), such as react
  2. cjs has a very mature, popular and highly used Runtime: node JS, and the runtime support of esm is very limited at present (the browser side needs advanced browser, and node needs some strange configuration and modification of file suffix)
  3. Many npm libraries are based on UMD, which is compatible with cjs, but because esm is static, UMD cannot be compatible with esm

Back to the question above. Open index. Of react library js:

You can see that react is based on cjs, which is equivalent to:

module.exports = {
  Children: Children,
  Component: Component
}

And in index In TS, write a paragraph

import React from "react";
console.log(React);

By default, the tsc compiled code is:

"use strict";
exports.__esModule = true;
var react_1 = require("react");
console.log(react_1["default"]);

Obviously, the printed result is undefined because the module of react There is no default and this attribute in exports. So get react later createElement,React.Component will naturally report an error.

The problem from this problem is that most of the existing third-party libraries are written in UMD / cjs (or their compiled products are generally cjs), but now the front-end code is basically written in esm, so esm and cjs need a set of rules to be compatible.

  • esm import esm

    • Both sides will be converted to cjs
    • Write in strict accordance with the standard of esm, and generally there will be no problems
  • esm import cjs

    • It is most common to refer to third-party libraries, such as react in this article
    • Compatibility problems arise because esm has the concept of default, but cjs does not. Any exported variable in cjs view is module Exports is the attribute on the object, and the default export of esm is only the module on cjs exports. It's just the default attribute
    • The importer esm will be converted to cjs
  • cjs import esm (generally not used in this way)
  • cjs import cjs

    • Will not be processed by the compiler
    • Write in strict accordance with the standard of cjs, and there will be no problem

TS default Compilation Rules

TS translation rules for import variables are:

 // before
 import React from 'react';
 console.log(React)
 // after
 var React = require('react');
 console.log(React['default'])


 // before
 import { Component } from 'react';
 console.log(Component);
 // after
 var React = require('react');
 console.log(React.Component)
 

 // before 
 import * as React from 'react';
 console.log(React);
 // after
 var React = require('react');
 console.log(React);

You can see:

  • For the module imported by import and exported by default, TS will read the above default attribute when reading this module
  • For import variables that are not exported by default, TS will read the corresponding attributes on this module
  • For import *, TS will read the module directly

The translation rules of TS and babel for export variables are: (the code has been simplified)

 // before
 export const name = "esm";
 export default {
   name: "esm default",
 };

 // after
 exports.__esModule = true;
 exports.name = "esm";
 exports["default"] = {
   name: "esm default"
 }

You can see:

  • For the variable of export default, TS will put it in module On the default attribute of exports
  • For export variables, TS will put them in module Exports is on the attribute of the corresponding variable name
  • Extra to module Exports add one__ Esmodule: the attribute of true, which is used to tell the compiler that this is originally an esm module

Compilation Rules after TS starts esModuleInterop

Return to the property "interop" by default. After changing to true, the TS translation rules for import will change (the export rules will not change):

 // before
 import React from 'react';
 console.log(React);
 // after code simplified
 var react = __importDefault(require('react'));
 console.log(react['default']);


 // before
 import {Component} from 'react';
 console.log(Component);
 // after code simplified
 var react = require('react');
 console.log(react.Component);
 
 
 // before
 import * as React from 'react';
 console.log(React);
 // after code simplified
 var react = _importStar(require('react'));
 console.log(react);

As you can see, TS uses two helper functions to help with the default import and namespace (*) import

// Code simplified
var __importDefault = function (mod) {
  return mod && mod.__esModule ? mod : { default: mod };
};

var __importStar = function (mod) {
  if (mod && mod.__esModule) {
    return mod;
  }

  var result = {};
  for (var k in mod) {
    if (k !== "default" && mod.hasOwnProperty(k)) {
      result[k] = mod[k]
    }
  }
  result["default"] = mod;

  return result;
};

First look__ importDefault. What it does is:

  1. If the target module is esm, return to the target module directly; Otherwise, hang the target module on the defalut of an object and return the object.

Like the one above

import React from 'react';

// ------

console.log(React);

After compilation, translate layer by layer:

// TS compilation
const React = __importDefault(require('react'));

// Translation require s
const React = __importDefault( { Children: Children, Component: Component } );

// Translate__ importDefault
const React = { default: { Children: Children, Component: Component } };

// -------

// Read React:
console.log(React.default);

// The last step is translation:
console.log({ Children: Children, Component: Component })

In this way, the module of react is successfully obtained exports.

Look again__ importStar. What it does is:

  1. If the target module is esm, return to the target module directly. otherwise
  2. Move all attributes except default on the target module to result
  3. Hang the target module to result On default

(like _importDefault above, the translation analysis process is omitted)

Rules for babel compilation

The default translation rule of babel is similar to that of TS when esModuleInterop is enabled. It is also handled through two helper functions

// before
import config from 'config';

console.log(config);
// after
"use strict";

var _config = _interopRequireDefault(require("config"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

console.log(_config["default"]);

// before
import * as config from 'config';

console.log(config);

// after
"use strict";

function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }

var config = _interopRequireWildcard(require("config"));

function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }

function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

console.log(config);

_ Interoprequireddefault is similar__ importDefault

_ interopRequireWildcard similar__ importStar

Module processing of webpack

In general development, babel and TS will be used together with webpack. Generally, there are two ways:

  • ts-loader
  • babel-loader

If TS loader is used, webpack will first hand over the source code to tsc for compilation, and then process the compiled code. After tsc compilation, all modules will become cjs, so babel will not process them, and will directly hand them over to webpack to process modules in the way of cjs. TS loader actually calls the tsc command, so you need tsconfig JSON configuration file

If babel loader is used, the web pack will not call tsc, tsconfig JSON will also be ignored. Instead, use babel to compile TS files directly. This compilation process is much lighter than calling tsc, because babel will simply remove all TS related code without type checking. Generally, in this case, a TS module is processed by @ babel / preset Env and @ babel / preset typescript of babel. What the latter does is very simple. It only removes all TS related code and does not process modules, while the former will convert esm into cjs. babel7 began to support compiling ts, which weakened the existence of tsc. The babel loader of webpack actually calls the babel command and needs babel config. JS configuration file

However, in webback.webback During transform, a caller option is passed:

As a result, babel retains the import and export of esm

tsc and babel can compile esm into cjs, but cjs can only run in the node environment, and webpack has its own set of module mechanism to deal with cjs, esm, AMD, UMD and other modules, and provide runtime for modules. Therefore, the code that needs to run in the browser finally needs to be modularized by webpack

For cjs referencing esm, the compilation mechanism of webpack is quite special:

// Code simplified
// before
import cjs from "./cjs";
console.log(cjs);
// after
var cjs = __webpack_require__("./src/cjs.js");
var cjsdefault = __webpack_require__.n(cjs);
console.log(cjsdefault.a);

// before
import esm from "./esm";
console.log(esm);
// after
var esm = __webpack_require__("./src/esm.js");
console.log(esm["default"]);

Among them__ webpack_require__ Similar to require, it returns the module of the target module Exports object__ webpack_require__.n this function receives a parameter object and returns an object. The a property of the returned object (I don't know why the property is called a) will be set as the parameter object. So the console of the above source code Log (CJS) will print out CJS JS module exports

Since webpack provides a runtime for the module, the processing module of webpack is very free for webpack itself. Just inject a variable representing module require exports into the module closure

Summary:

At present, many commonly used packages are developed based on cjs / UMD, while the front-end code is usually written in esm, so the common scenario is that esm is imported into cjs library. However, there are 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 mechanism to deal with this compatibility problem. The core idea is basically to add and read the default attribute

reference resources

What did esmodule interop do?

Keywords: TypeScript Webpack import

Added by ditusade on Fri, 11 Feb 2022 06:02:17 +0200