Why Does Vite Need to Rewrite Pre-build Module Paths?
The hook for rewriting imports is transformCjsImport
, with the following source code comments:
/**
* 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
## Uncaught SyntaxError: The requested module '/@modules/redux-dynamic-modules.js' does not provide an export named 'createStore'
Firefox
## Uncaught SyntaxError: import not found: createStore
Prerequisites
There are differences between importing
ESM
andCJS
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.
Typically,
Vite
will pre-build modules (usually CommonJS modules) that are eitherBare Import
or configured inconfig.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.
/*======== 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:
// 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';
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 istrue
, we take itsdefault
value; iffalse
, 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, theCommonJS
module specification was commonly used.Node
is a typical example, natively supportingCommonJS
, so early libraries mostly supportedCommonJS
(or AMD, etc.). Later, when the official promoted theESM
modularization solution, problems arose when mixing the two module formats.ES modules
andCommonJS modules
are not fully compatible, andCommonJS
'smodule.exports
has no corresponding expression inES modules
, which is different from default exportsexport default
. That is, inESM
, you cannot use default imports forCommonJS
.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 byBabel
, and now build tools in the market have tacitly followed this convention. The convention is as follows:Babel
first introduces the__esModule
identifier inCommonJS
module files. If__esModule
istrue
, then whenCommonJS
is converted toESM
, the value exported byexport default
is the value ofmodule.exports.default
. If__esModule
isfalse
, then whenCommonJS
is converted toESM
, the value ofexport default
is the value of the entiremodule.exports
. By following these rules, you can default importCommonJS
modules inESM
.jsexports.__esModule = true; // or Object.defineProperty(exports, '__esModule', { value: true });
Points to note:
If
__esModule
isfalse
in aCommonJS
module, the entiremodule.exports
object is exported. If__esModule
is set tofalse
, this object might have an extra__esModule
property. Therefore, if__esModule
isfalse
, it doesn't need to be set. It can be considered that__esModule
isfalse
by default.If
__esModule
istrue
in aCommonJS
module but there is nomodule.exports.default
property. For this case, different build tools may have different behaviors.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;
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;
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 supportsESM
innodejs
. At this time, if you referenceCommonJS
in files ending with.mjs
, it generally won't do special processing. The purpose of__esModule
itself is to be compatible with thenodejs
environment, allowingESM
to run in thenodejs
environment. Therefore,__esModule
won't take effect here, and all properties are treated as ordinary properties.Rewrite
import * as React from 'react'
. The rewriting method is similar to the first default export, but different in thatimport *
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;
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;
Rewrite dynamic imports (
import(...)
) to directly expose thedefault
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 } );
Rewrite re-exported modules, i.e., import then export.
jsexport { 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 };
Rewrite re-exported modules.
jsexport * as React from 'react'; // rewrite =============> export * as React from '/node_modules/.vite/deps/react.js?v=3c90f486';
export * from 'react'
has an error and may lose module exports. We can see this comment in thetransformCjsImport
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:
- The
export * from '@prisma/client'
syntax means exporting all non-default
data collections from the@prisma/client
module, that is, usingexport
to export inESM
(notexport default
). @prisma/client
is detected as a module that needs pre-building inVite
(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 thatESbuild
's build product ofCommonJS
will ultimately be exported asexport default
. This means thatexport * from 'CommonJS_Module'
is meaningless (the build product won't export properties usingexport
). This explains why bothimport { UserRole } from './bug.ts'
andimport UserRole from "./bug.ts"
report the same error. The former is because there is noexport
specifiedUserRole
property in@prisma/client
, and the latter is becauseexport * from 'xxx'
exports non-default
data collections. Both are empty property collections, so naturally, you can't get theUserRole
anddefault
properties.- 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:
tsimport { 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 theESM
product already built byVite
, exported by default usingexport default
, so it can execute successfully.- The
import 'react'
import 'react';
// rewrite =============>
import '/node_modules/.vite/deps/react.js?v=3c90f486';