@@ -11,7 +11,12 @@ import { lookupPackageScopeType } from '@vitest/utils/resolver'
1111import { extname , normalize } from 'pathe'
1212import { CommonjsExecutor } from './vm/commonjs-executor'
1313import { EsmExecutor } from './vm/esm-executor'
14- import { setActiveVmExecutor } from './vm/utils'
14+ import {
15+ createRequireAsyncModuleError ,
16+ hasEsmSyntax ,
17+ setActiveVmExecutor ,
18+ supportsSyncEsmEvaluate ,
19+ } from './vm/utils'
1520import { ViteExecutor } from './vm/vite-executor'
1621
1722const { existsSync } = fs
@@ -52,6 +57,14 @@ export interface ModuleInformation {
5257 exists ?: boolean
5358}
5459
60+ // how the sync require(esm) graph walker should treat a resolved module:
61+ // 'ready' modules are complete synthetic modules (builtins, CJS files),
62+ // 'source'/'json' carry the raw content for the walker to build itself
63+ export type SyncModuleDisposition
64+ = | { kind : 'ready' ; module : VMModule }
65+ | { kind : 'json' ; code : string }
66+ | { kind : 'source' ; code : string }
67+
5568// TODO: improve Node.js strict mode support in #2854
5669export class ExternalModulesExecutor {
5770 private cjs : CommonjsExecutor
@@ -79,6 +92,8 @@ export class ExternalModulesExecutor {
7992 fileMap : options . fileMap ,
8093 codeCache : options . codeCache ,
8194 interopDefault : options . interopDefault ,
95+ shouldRequireAsEsm : this . shouldRequireAsEsm ,
96+ requireEsm : this . requireEsm ,
8297 } )
8398 this . vite = new ViteExecutor ( {
8499 esmExecutor : this . esm ,
@@ -103,6 +118,122 @@ export class ExternalModulesExecutor {
103118 return this . cjs . createRequire ( identifier )
104119 }
105120
121+ #esmSyntaxCache = new Map < string , boolean > ( )
122+
123+ // require() dispatches to the sync ESM loader only for files that are
124+ // explicitly marked as ESM (.mjs or a "type": "module" package scope).
125+ // JSON files keep the CJS json loader for Node require() parity — the
126+ // extension wins over the package scope.
127+ private shouldRequireAsEsm = ( resolvedPath : string ) : boolean => {
128+ if ( ! supportsSyncEsmEvaluate ) {
129+ return false
130+ }
131+ const information = this . getModuleInformation ( resolvedPath )
132+ if ( information . type !== 'module' || information . path . endsWith ( '.json' ) ) {
133+ return false
134+ }
135+ if ( information . path . endsWith ( '.mjs' ) ) {
136+ return true
137+ }
138+ // A .js file in an ESM package scope may still contain plain CJS code —
139+ // Node evaluates it as ESM with injected CJS module variables (module,
140+ // require, __filename), which a vm SourceTextModule cannot emulate.
141+ // Files without ESM syntax keep loading through the CJS executor; a
142+ // false negative here is corrected by its ESM-syntax fallback.
143+ let syntax = this . #esmSyntaxCache. get ( information . path )
144+ if ( syntax == null ) {
145+ syntax = hasEsmSyntax ( this . fs . readFile ( information . path ) )
146+ this . #esmSyntaxCache. set ( information . path , syntax )
147+ }
148+ return syntax
149+ }
150+
151+ private requireEsm = ( resolvedPath : string ) : unknown => {
152+ const { url } = this . getModuleInformation ( resolvedPath )
153+ const module = this . esm . requireEsModuleSync ( url )
154+ const namespace = module . namespace as Record < string , unknown >
155+ // Node parity: an ES module can define its own require() result with an
156+ // export named "module.exports"
157+ return 'module.exports' in namespace
158+ ? namespace [ 'module.exports' ]
159+ : namespace
160+ }
161+
162+ public resolveSyncSpecifier = (
163+ specifier : string ,
164+ referencer : string ,
165+ ) : string => {
166+ const resolved = this . resolve ( specifier , referencer ) as
167+ | string
168+ | Promise < string >
169+ if ( resolved instanceof Promise ) {
170+ throw createRequireAsyncModuleError (
171+ referencer ,
172+ `"${ specifier } " cannot be resolved synchronously` ,
173+ )
174+ }
175+ return resolved
176+ }
177+
178+ // the sync counterpart of `createModule`, used by the require(esm) graph
179+ // walker. `forceEsmSource` loads a 'commonjs'-typed file as ES module
180+ // source — the CJS executor requests this after its parser rejected a .js
181+ // file that contains ESM syntax.
182+ public materializeSyncModule = (
183+ identifier : string ,
184+ forceEsmSource : boolean ,
185+ ) : SyncModuleDisposition => {
186+ const information = this . getModuleInformation ( identifier )
187+ const { type, path } = information
188+ this . assertModuleExists ( information )
189+
190+ switch ( type ) {
191+ case 'builtin' :
192+ return {
193+ kind : 'ready' ,
194+ module : this . cjs . getCoreSyntheticModule ( identifier ) ,
195+ }
196+ case 'module' :
197+ case 'commonjs' : {
198+ if ( type === 'commonjs' && ! forceEsmSource ) {
199+ return {
200+ kind : 'ready' ,
201+ module : this . cjs . getCjsSyntheticModule ( path , identifier ) ,
202+ }
203+ }
204+ if ( path . endsWith ( '.json' ) ) {
205+ return { kind : 'json' , code : this . fs . readFile ( path ) }
206+ }
207+ return { kind : 'source' , code : this . fs . readFile ( path ) }
208+ }
209+ case 'data' :
210+ // data: URIs are materialized by the ESM executor before it consults
211+ // the external executor
212+ throw new Error (
213+ `[vitest] Unexpected data: module ${ identifier } in the sync module walker. This is a bug in Vitest.` ,
214+ )
215+ case 'vite' :
216+ throw createRequireAsyncModuleError (
217+ identifier ,
218+ 'the module is transformed by Vite, which is asynchronous' ,
219+ )
220+ case 'wasm' :
221+ throw createRequireAsyncModuleError (
222+ identifier ,
223+ 'WebAssembly modules cannot be loaded synchronously' ,
224+ )
225+ case 'network' :
226+ throw createRequireAsyncModuleError (
227+ identifier ,
228+ 'network modules cannot be loaded synchronously' ,
229+ )
230+ default : {
231+ const _deadend : never = type
232+ return _deadend
233+ }
234+ }
235+ }
236+
106237 // dynamic import can be used in both ESM and CJS, so we have it in the executor
107238 public importModuleDynamically = async (
108239 specifier : string ,
@@ -213,20 +344,25 @@ export class ExternalModulesExecutor {
213344 return { type, path : pathUrl , url : fileUrl }
214345 }
215346
216- private createModule ( identifier : string ) : VMModule | Promise < VMModule > {
217- const information = this . getModuleInformation ( identifier )
218- const { type, url, path } = information
219-
220- // create ERR_MODULE_NOT_FOUND on our own since latest NodeJS's import.meta.resolve doesn't throw on non-existing namespace or path
221- // https://github.com/nodejs/node/pull/49038
347+ // create ERR_MODULE_NOT_FOUND on our own since latest NodeJS's import.meta.resolve doesn't throw on non-existing namespace or path
348+ // https://github.com/nodejs/node/pull/49038
349+ private assertModuleExists ( information : ModuleInformation ) : void {
350+ const { type, path } = information
222351 if ( type === 'module' || type === 'commonjs' || type === 'wasm' ) {
223352 information . exists ??= existsSync ( path )
224353 }
225354 if ( information . exists === false ) {
226- const error = new Error ( `Cannot find ${ isBareImport ( path ) ? 'package' : 'module' } '${ path } '` ) ;
227- ( error as any ) . code = 'ERR_MODULE_NOT_FOUND'
355+ const error : NodeJS . ErrnoException = new Error ( `Cannot find ${ isBareImport ( path ) ? 'package' : 'module' } '${ path } '` )
356+ error . code = 'ERR_MODULE_NOT_FOUND'
228357 throw error
229358 }
359+ }
360+
361+ private createModule ( identifier : string ) : VMModule | Promise < VMModule > {
362+ const information = this . getModuleInformation ( identifier )
363+ const { type, url, path } = information
364+
365+ this . assertModuleExists ( information )
230366
231367 switch ( type ) {
232368 case 'data' :
@@ -236,11 +372,9 @@ export class ExternalModulesExecutor {
236372 case 'vite' :
237373 return this . vite . createViteModule ( url )
238374 case 'wasm' :
239- return this . esm . createWebAssemblyModule ( url , ( ) =>
240- this . fs . readBuffer ( path ) )
375+ return this . esm . createWebAssemblyModule ( url , ( ) => this . fs . readBuffer ( path ) )
241376 case 'module' :
242- return this . esm . createEsModule ( url , ( ) =>
243- this . fs . readFileAsync ( path ) )
377+ return this . esm . createEsModule ( url , ( ) => this . fs . readFile ( path ) )
244378 case 'commonjs' :
245379 return this . cjs . getCjsSyntheticModule ( path , identifier )
246380 case 'network' :
0 commit comments