Skip to content

Why Does Vite Need to Rewrite Pre-build Module Paths?

The hook for rewriting imports is transformCjsImport, with the following source code comments:

js
/**
 * Detect import statements to a known optimized CJS dependency and provide
 * ES named imports interop. We do this by rewriting named imports to a variable
 * assignment to the corresponding property on the `module.exports` of the cjs
 * module. Note this doesn't support dynamic re-assignments from within the cjs
 * module.
 *
 * Note that es-module-lexer treats `export * from '...'` as an import as well,
 * so, we may encounter ExportAllDeclaration here, in which case `undefined`
 * will be returned.
 *
 * Credits \@csr632 via #837
 */

The root of the problem is issue#837. Through tracing, we can find issue#720, which describes the error phenomenon when using named imports in Vite.

Chrome

markdown
## Uncaught SyntaxError: The requested module '/@modules/redux-dynamic-modules.js' does not provide an export named 'createStore'

Firefox

markdown
## Uncaught SyntaxError: import not found: createStore

Prerequisites

  1. There are differences between importing ESM and CJS specification modules.

    In ESM:

    js
    // index.js
    import demo from 'demo.js';
    
    // demo.js
    export default {
      name: 'demo.js'
    };
    
    export const name = 'demo.js';

    The above code is compliant with the specification.

    js
    // index.js
    import { name } from 'demo.js';
    
    // demo.js
    export default {
      name: 'demo.js'
    };
    
    export const p = 'demo.js';

    The above code is not compliant with the specification. Named imports must correspond to named exports, meaning it must include:

    js
    // index.js
    import { name } from 'demo.js';
    
    // demo.js
    export const name = 'demo.js';

    This meets the requirements.

    In CJS:

    js
    // index.js
    import { name } from 'demo.js';
    
    // demo.js
    module.exports = {
      name: 'demo.js'
    };

    The above code is compliant with the specification.

  2. Typically, Vite will pre-build modules (usually CommonJS modules) that are either Bare Import or configured in config.optimizeDeps.include (CommonJS -> ESM).

With these two prerequisites, it's not difficult to understand the official comments. Simply put:

Since ESM will likely import pre-built products (CommonJS modules) in most cases, and these modules have already been bundled into ESM specification modules through Esbuild, it's no longer possible to import the default product through named imports.

js
/*======== origin ========*/

// index.js
import { name } from 'demo';

// demo.js
module.exports = {
  name: 'demo.js'
};


/*======== after bundle demo.js ========*/

error;
// index.js
import { name } from 'demo';

// demo.js
export default {
  name: 'demo.js'
};

success;
// index.js [rewrite path]
import uniqueName from 'demo';
const name = uniqueName.name;

// demo.js
export default {
  name: 'demo.js'
};

How to Rewrite Pre-built Module Paths?

In ESM, there are several import methods, so we only need to handle the following import methods:

js
// 1.
import React from 'react';

// 2.
import * as React from 'react';

// 3.
import { jsxDEV as _jsxDEV, Fragment } from 'react/jsx-dev-runtime';

// 4.
import('react');

// 5.
export { useState, useEffect as effect, useMemo as default } from 'react';

// 6.
export * as React from 'react';

// 7. Error!
export * from 'react';

// 8.
import 'react';
  1. Rewrite default imports (import React from 'react'). For default imports, we mainly consider the __esModule property value in the CommonJS bundled product. If the value is true, we take its default value; if false, we take its own value.

    js
    // `const ${localName} = ${cjsModuleName}.__esModule ? ${cjsModuleName}.default : ${cjsModuleName}`
    
    import React from 'react';
    
    // rewrite =============>
    
    import __vite__cjsImport0_react from '/node_modules/.vite/deps/react.js?v=048661c9';
    const React = __vite__cjsImport0_react.__esModule
      ? __vite__cjsImport0_react.default
      : __vite__cjsImport0_react;

    Why do we need the __esModule property?

    Historical baggage:

    Before the ESM module specification was introduced, the CommonJS module specification was commonly used. Node is a typical example, natively supporting CommonJS, so early libraries mostly supported CommonJS (or AMD, etc.). Later, when the official promoted the ESM modularization solution, problems arose when mixing the two module formats. ES modules and CommonJS modules are not fully compatible, and CommonJS's module.exports has no corresponding expression in ES modules, which is different from default exports export default. That is, in ESM, you cannot use default imports for CommonJS.

    js
    // Bad Case: index.js
    import demo from './demo.js';
    
    // Good Case: index.js
    import { name } from './demo.js';
    import * as demo from './demo.js';
    
    // demo.js
    module.exports = {
      name: 'demo'
    };

    Solution:

    The __esModule solution was first proposed by Babel, and now build tools in the market have tacitly followed this convention. The convention is as follows:

    Babel first introduces the __esModule identifier in CommonJS module files. If __esModule is true, then when CommonJS is converted to ESM, the value exported by export default is the value of module.exports.default. If __esModule is false, then when CommonJS is converted to ESM, the value of export default is the value of the entire module.exports. By following these rules, you can default import CommonJS modules in ESM.

    js
    exports.__esModule = true;
    
    // or
    
    Object.defineProperty(exports, '__esModule', { value: true });

    Points to note:

    1. If __esModule is false in a CommonJS module, the entire module.exports object is exported. If __esModule is set to false, this object might have an extra __esModule property. Therefore, if __esModule is false, it doesn't need to be set. It can be considered that __esModule is false by default.

    2. If __esModule is true in a CommonJS module but there is no module.exports.default property. For this case, different build tools may have different behaviors.

      1. In ESbuild:

        js
        //commonjs a.js
        module.exports.a = 2;
        module.exports.b = 3;
        module.exports.__esModule = true;
        
        //main.js
        
        import x from './a.js';
        x = undefined;
      2. In Vite:

      js
      // `const ${localName} = ${cjsModuleName}.__esModule ? ${cjsModuleName}.default : ${cjsModuleName}`
      
      import React from 'react';
      
      // rewrite =============>
      
      import __vite__cjsImport0_react from '/node_modules/.vite/deps/react.js?v=048661c9';
      
      // React = undefined
      const React = __vite__cjsImport0_react.__esModule
        ? __vite__cjsImport0_react.default
        : __vite__cjsImport0_react;
    3. When importing in .mjs files:

    js
    //commonjs a.js
    module.exports.default = 'aa';
    module.exports.a = 2;
    module.exports.b = 3;
    module.exports.__esModule = true;
    
    //main.mjs
    
    import x from './a.js';
    x = {
      default: 'aa',
      a: 2,
      b: 2,
      __esModule: true
    };

    Files ending with .mjs are the form that supports ESM in nodejs. At this time, if you reference CommonJS in files ending with .mjs, it generally won't do special processing. The purpose of __esModule itself is to be compatible with the nodejs environment, allowing ESM to run in the nodejs environment. Therefore, __esModule won't take effect here, and all properties are treated as ordinary properties.

  2. Rewrite import * as React from 'react'. The rewriting method is similar to the first default export, but different in that import * essentially needs to import all properties of the module, so there's no need to consider the __esModule property value.

    js
    // const ${localName} = ${cjsModuleName}
    
    import * as React from 'react';
    
    // rewrite =============>
    
    import __vite__cjsImport0_react from '/node_modules/.vite/deps/react.js?v=048661c9';
    const React = __vite__cjsImport0_react;
  3. Rewrite named imports:

    js
    // const ${localName} = ${cjsModuleName}["${importedName}"]
    
    import { jsxDEV as _jsxDEV, Fragment } from 'react/jsx-dev-runtime';
    
    // rewrite =============>
    
    import __vite__cjsImport0_react_jsxDevRuntime from '/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=65945471';
    const _jsxDEV = __vite__cjsImport0_react_jsxDevRuntime.jsxDEV;
    const Fragment = __vite__cjsImport0_react_jsxDevRuntime.Fragment;
  4. Rewrite dynamic imports (import(...)) to directly expose the default value.

    js
    // import('${rewrittenUrl}').then(m => m.default && m.default.__esModule ? m.default : ({ ...m.default, default: m.default }))
    
    import('react');
    
    // rewrite =============>
    
    import(
      '/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=65945471'
    ).then(m =>
      m.default && m.default.__esModule
        ? m.default
        : { ...m.default, default: m.default }
    );
  5. Rewrite re-exported modules, i.e., import then export.

    js
    export { useState, useEffect as effect, useMemo as default } from 'react';
    
    // rewrite =============>
    
    import __vite__cjsImport0_react from '/node_modules/.vite/deps/react.js?v=3c90f486';
    const __vite__cjsExport_useState = __vite__cjsImport0_react.useState;
    const __vite__cjsExport_effect = __vite__cjsImport0_react.useEffect;
    const __vite__cjsExportDefault_0 = __vite__cjsImport0_react.useMemo;
    export default __vite__cjsExportDefault_0;
    export { __vite__cjsExport_useState as useState, __vite__cjsExport_effect as effect };
  6. Rewrite re-exported modules.

    js
    export * as React from 'react';
    
    // rewrite =============>
    
    export * as React from '/node_modules/.vite/deps/react.js?v=3c90f486';
  7. export * from 'react' has an error and may lose module exports. We can see this comment in the transformCjsImport method:

    js
    // `export * from '...'` may cause unexpected problem, so give it a warning
    if (
      config.command === 'serve' &&
      node.type === 'ExportAllDeclaration' &&
      !node.exported
    ) {
      config.logger.warn(
        colors.yellow(
          `\nUnable to interop \`${importExp}\` in ${importer}, this may lose module exports. Please export "${rawUrl}" as ESM or use named exports instead, e.g. \`export { A, B } from "${rawUrl}"\``
        )
      );
    }

    So what causes this? We can trace back to this issue. Let's briefly explain the problem described in this issue:

    Problem: In App.tsx, you cannot use named imports for re-exported content, but you can import normally through direct imports.

    js
    // bug.ts
    export * from '@prisma/client';
    js
    // App.tsx
    import { UserRole } from './bug.ts';
    // Syntax error on runtime "The requested module bug.ts does not provide an export UserRole"
    
    import UserRole from './bug.ts';
    // Syntax error on runtime "The requested module bug.ts does not provide an export named default"
    
    import { UserRole } from '@prisma/client'; // Works fine

    Cause Analysis:

    1. The export * from '@prisma/client' syntax means exporting all non-default data collections from the @prisma/client module, that is, using export to export in ESM (not export default).
    2. @prisma/client is detected as a module that needs pre-building in Vite (a CommonJS module). During the pre-build phase, ESbuild will build the original CommonJS module into an ESM module. By observing the build product, we can see that ESbuild's build product of CommonJS will ultimately be exported as export default. This means that export * from 'CommonJS_Module' is meaningless (the build product won't export properties using export). This explains why both import { UserRole } from './bug.ts' and import UserRole from "./bug.ts" report the same error. The former is because there is no export specified UserRole property in @prisma/client, and the latter is because export * from 'xxx' exports non-default data collections. Both are empty property collections, so naturally, you can't get the UserRole and default properties.
    3. The reason why the third writing method works is that Vite determines @prisma/client as a pre-built product, so it will rewrite the path:
    ts
    import { UserRole } from '@prisma/client';
    
    // rewrite ========>
    
    import __vite__cjsImport0_prisma_client from '/node_modules/.vite/deps/@prisma_client.js?v=65945471';
    const UserRole = __vite__cjsImport0_prisma_client.UserRole;

    /node_modules/.vite/deps/@prisma_client.js?v=65945471 is the ESM product already built by Vite, exported by default using export default, so it can execute successfully.

  8. import 'react'

js
import 'react';

// rewrite =============>

import '/node_modules/.vite/deps/react.js?v=3c90f486';

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (dbcbf17)