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: createStorePrerequisites
There are differences between importing
ESMandCJSspecification 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,
Vitewill pre-build modules (usually CommonJS modules) that are eitherBare Importor 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__esModuleproperty value in the CommonJS bundled product. If the value istrue, we take itsdefaultvalue; 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
__esModuleproperty?Historical baggage:
Before the
ESMmodule specification was introduced, theCommonJSmodule specification was commonly used.Nodeis a typical example, natively supportingCommonJS, so early libraries mostly supportedCommonJS(or AMD, etc.). Later, when the official promoted theESMmodularization solution, problems arose when mixing the two module formats.ES modulesandCommonJS modulesare not fully compatible, andCommonJS'smodule.exportshas 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
__esModulesolution was first proposed byBabel, and now build tools in the market have tacitly followed this convention. The convention is as follows:Babelfirst introduces the__esModuleidentifier inCommonJSmodule files. If__esModuleistrue, then whenCommonJSis converted toESM, the value exported byexport defaultis the value ofmodule.exports.default. If__esModuleisfalse, then whenCommonJSis converted toESM, the value ofexport defaultis the value of the entiremodule.exports. By following these rules, you can default importCommonJSmodules inESM.jsexports.__esModule = true; // or Object.defineProperty(exports, '__esModule', { value: true });Points to note:
If
__esModuleisfalsein aCommonJSmodule, the entiremodule.exportsobject is exported. If__esModuleis set tofalse, this object might have an extra__esModuleproperty. Therefore, if__esModuleisfalse, it doesn't need to be set. It can be considered that__esModuleisfalseby default.If
__esModuleistruein aCommonJSmodule but there is nomodule.exports.defaultproperty. 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
.mjsfiles:
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
.mjsare the form that supportsESMinnodejs. At this time, if you referenceCommonJSin files ending with.mjs, it generally won't do special processing. The purpose of__esModuleitself is to be compatible with thenodejsenvironment, allowingESMto run in thenodejsenvironment. Therefore,__esModulewon'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__esModuleproperty 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 thedefaultvalue.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 thetransformCjsImportmethod: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 fineCause Analysis:
- The
export * from '@prisma/client'syntax means exporting all non-defaultdata collections from the@prisma/clientmodule, that is, usingexportto export inESM(notexport default). @prisma/clientis detected as a module that needs pre-building inVite(a CommonJS module). During the pre-build phase,ESbuildwill build the original CommonJS module into an ESM module. By observing the build product, we can see thatESbuild's build product ofCommonJSwill 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 noexportspecifiedUserRoleproperty in@prisma/client, and the latter is becauseexport * from 'xxx'exports non-defaultdata collections. Both are empty property collections, so naturally, you can't get theUserRoleanddefaultproperties.- The reason why the third writing method works is that
Vitedetermines@prisma/clientas 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=65945471is theESMproduct 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';