@@ -24,8 +24,13 @@ const SUGGEST_NODE =
2424 "that uses Node.js built-ins, you'll either need to:" +
2525 "\n- Bundle your Worker, configuring your bundler to polyfill Node.js built-ins" +
2626 "\n- Configure your bundler to load Workers-compatible builds by changing the main fields/conditions" +
27+ "\n- Enable the `nodejs_compat` compatibility flag and use the `NodeJsCompatModule` module type" +
2728 "\n- Find an alternative package that doesn't require Node.js built-ins" ;
2829
30+ const builtinModulesWithPrefix = builtinModules . concat (
31+ builtinModules . map ( ( module ) => `node:${ module } ` )
32+ ) ;
33+
2934// Module identifier used if script came from `script` option
3035export function buildStringScriptPath ( workerIndex : number ) {
3136 return `<script:${ workerIndex } >` ;
@@ -38,15 +43,18 @@ export function maybeGetStringScriptPathIndex(
3843 return match === null ? undefined : parseInt ( match [ 1 ] ) ;
3944}
4045
41- export const ModuleRuleTypeSchema = z . union ( [
42- z . literal ( "ESModule" ) ,
43- z . literal ( "CommonJS" ) ,
44- z . literal ( "Text" ) ,
45- z . literal ( "Data" ) ,
46- z . literal ( "CompiledWasm" ) ,
46+ export const ModuleRuleTypeSchema = z . enum ( [
47+ "ESModule" ,
48+ "CommonJS" ,
49+ "NodeJsCompatModule" ,
50+ "Text" ,
51+ "Data" ,
52+ "CompiledWasm" ,
4753] ) ;
4854export type ModuleRuleType = z . infer < typeof ModuleRuleTypeSchema > ;
4955
56+ type JavaScriptModuleRuleType = "ESModule" | "CommonJS" | "NodeJsCompatModule" ;
57+
5058export const ModuleRuleSchema = z . object ( {
5159 type : ModuleRuleTypeSchema ,
5260 include : z . string ( ) . array ( ) ,
@@ -130,16 +138,22 @@ function getResolveErrorPrefix(referencingPath: string): string {
130138
131139export class ModuleLocator {
132140 readonly #compiledRules: CompiledModuleRule [ ] ;
141+ readonly #nodejsCompat: boolean ;
133142 readonly #visitedPaths = new Set < string > ( ) ;
134143 readonly modules : Worker_Module [ ] = [ ] ;
135144
136145 constructor (
137146 private readonly sourceMapRegistry : SourceMapRegistry ,
138147 private readonly modulesRoot : string ,
139148 private readonly additionalModuleNames : string [ ] ,
140- rules ?: ModuleRule [ ]
149+ rules ?: ModuleRule [ ] ,
150+ compatibilityFlags ?: string [ ]
141151 ) {
142152 this . #compiledRules = compileModuleRules ( rules ) ;
153+ // `nodejs_compat` doesn't have a default-on date, so we know whether it's
154+ // enabled just by looking at flags:
155+ // https://github.com/cloudflare/workerd/blob/edcd0300bc7b8f56040d090177db947edd22f91b/src/workerd/io/compatibility-date.capnp#L237-L240
156+ this . #nodejsCompat = compatibilityFlags ?. includes ( "nodejs_compat" ) ?? false ;
143157 }
144158
145159 visitEntrypoint ( code : string , modulePath : string ) {
@@ -150,23 +164,32 @@ export class ModuleLocator {
150164 this . #visitedPaths. add ( modulePath ) ;
151165
152166 // Entrypoint is always an ES module
153- this . #visitJavaScriptModule( code , modulePath ) ;
167+ this . #visitJavaScriptModule( code , modulePath , "ESModule" ) ;
154168 }
155169
156- #visitJavaScriptModule( code : string , modulePath : string , esModule = true ) {
170+ #visitJavaScriptModule(
171+ code : string ,
172+ modulePath : string ,
173+ type : JavaScriptModuleRuleType
174+ ) {
157175 // Register module
158176 const name = path . relative ( this . modulesRoot , modulePath ) ;
159- code = this . sourceMapRegistry . register ( code , modulePath ) ;
160- this . modules . push (
161- esModule ? { name, esModule : code } : { name, commonJsModule : code }
177+ const module = createJavaScriptModule (
178+ this . sourceMapRegistry ,
179+ code ,
180+ name ,
181+ modulePath ,
182+ type
162183 ) ;
184+ this . modules . push ( module ) ;
163185
164186 // Parse code and visit all import/export statements
187+ const isESM = type === "ESModule" ;
165188 let root ;
166189 try {
167190 root = parse ( code , {
168191 ecmaVersion : "latest" ,
169- sourceType : esModule ? "module" : "script" ,
192+ sourceType : isESM ? "module" : "script" ,
170193 locations : true ,
171194 } ) ;
172195 } catch ( e : any ) {
@@ -187,18 +210,20 @@ export class ModuleLocator {
187210 // noinspection JSUnusedGlobalSymbols
188211 const visitors = {
189212 ImportDeclaration : ( node : estree . ImportDeclaration ) => {
190- this . #visitModule( modulePath , node . source ) ;
213+ this . #visitModule( modulePath , type , node . source ) ;
191214 } ,
192215 ExportNamedDeclaration : ( node : estree . ExportNamedDeclaration ) => {
193- if ( node . source != null ) this . #visitModule( modulePath , node . source ) ;
216+ if ( node . source != null ) {
217+ this . #visitModule( modulePath , type , node . source ) ;
218+ }
194219 } ,
195220 ExportAllDeclaration : ( node : estree . ExportAllDeclaration ) => {
196- this . #visitModule( modulePath , node . source ) ;
221+ this . #visitModule( modulePath , type , node . source ) ;
197222 } ,
198223 ImportExpression : ( node : estree . ImportExpression ) => {
199- this . #visitModule( modulePath , node . source ) ;
224+ this . #visitModule( modulePath , type , node . source ) ;
200225 } ,
201- CallExpression : esModule
226+ CallExpression : isESM
202227 ? undefined
203228 : ( node : estree . CallExpression ) => {
204229 // TODO: check global?
@@ -208,7 +233,7 @@ export class ModuleLocator {
208233 node . callee . name === "require" &&
209234 argument !== undefined
210235 ) {
211- this . #visitModule( modulePath , argument ) ;
236+ this . #visitModule( modulePath , type , argument ) ;
212237 }
213238 } ,
214239 } ;
@@ -217,6 +242,7 @@ export class ModuleLocator {
217242
218243 #visitModule(
219244 referencingPath : string ,
245+ referencingType : JavaScriptModuleRuleType ,
220246 specExpression : estree . Expression | estree . SpreadElement
221247 ) {
222248 if ( maybeGetStringScriptPathIndex ( referencingPath ) !== undefined ) {
@@ -238,8 +264,10 @@ export class ModuleLocator {
238264 return ` { type: "${ def . type } ", path: "${ def . path } " }` ;
239265 } ) ;
240266 const modulesConfig = ` new Miniflare({
267+ ...,
241268 modules: [
242- ${ modules . join ( ",\n" ) }
269+ ${ modules . join ( ",\n" ) } ,
270+ ...
243271 ]
244272 })` ;
245273
@@ -257,12 +285,14 @@ ${dim(modulesConfig)}`;
257285 }
258286 const spec = specExpression . value ;
259287
260- // `node:`, `cloudflare:` and `workerd:` imports don't need to be included
261- // explicitly
288+ // `node:` (assuming `nodejs_compat` flag enabled), `cloudflare:` and
289+ // `workerd:` imports don't need to be included explicitly
290+ const isNodeJsCompatModule = referencingType === "NodeJsCompatModule" ;
262291 if (
263- spec . startsWith ( "node:" ) ||
292+ ( this . #nodejsCompat && spec . startsWith ( "node:" ) ) ||
264293 spec . startsWith ( "cloudflare:" ) ||
265294 spec . startsWith ( "workerd:" ) ||
295+ ( isNodeJsCompatModule && builtinModulesWithPrefix . includes ( spec ) ) ||
266296 this . additionalModuleNames . includes ( spec )
267297 ) {
268298 return ;
@@ -282,7 +312,7 @@ ${dim(modulesConfig)}`;
282312 ) ;
283313 if ( rule === undefined ) {
284314 const prefix = getResolveErrorPrefix ( referencingPath ) ;
285- const isBuiltin = builtinModules . includes ( spec ) ;
315+ const isBuiltin = builtinModulesWithPrefix . includes ( spec ) ;
286316 const suggestion = isBuiltin ? SUGGEST_NODE : SUGGEST_BUNDLE ;
287317 throw new MiniflareCoreError (
288318 "ERR_MODULE_RULE" ,
@@ -294,10 +324,10 @@ ${dim(modulesConfig)}`;
294324 const data = readFileSync ( identifier ) ;
295325 switch ( rule . type ) {
296326 case "ESModule" :
297- this . #visitJavaScriptModule( data . toString ( "utf8" ) , identifier ) ;
298- break ;
299327 case "CommonJS" :
300- this . #visitJavaScriptModule( data . toString ( "utf8" ) , identifier , false ) ;
328+ case "NodeJsCompatModule" :
329+ const code = data . toString ( "utf8" ) ;
330+ this . #visitJavaScriptModule( code , identifier , rule . type ) ;
301331 break ;
302332 case "Text" :
303333 this . modules . push ( { name, text : data . toString ( "utf8" ) } ) ;
@@ -310,11 +340,32 @@ ${dim(modulesConfig)}`;
310340 break ;
311341 default :
312342 // `type` should've been validated against `ModuleRuleTypeSchema`
313- assert . fail ( `Unreachable: ${ rule . type } modules are unsupported` ) ;
343+ const exhaustive : never = rule . type ;
344+ assert . fail ( `Unreachable: ${ exhaustive } modules are unsupported` ) ;
314345 }
315346 }
316347}
317348
349+ function createJavaScriptModule (
350+ sourceMapRegistry : SourceMapRegistry ,
351+ code : string ,
352+ name : string ,
353+ modulePath : string ,
354+ type : JavaScriptModuleRuleType
355+ ) : Worker_Module {
356+ code = sourceMapRegistry . register ( code , modulePath ) ;
357+ if ( type === "ESModule" ) {
358+ return { name, esModule : code } ;
359+ } else if ( type === "CommonJS" ) {
360+ return { name, commonJsModule : code } ;
361+ } else if ( type === "NodeJsCompatModule" ) {
362+ return { name, nodeJsCompatModule : code } ;
363+ }
364+ // noinspection UnnecessaryLocalVariableJS
365+ const exhaustive : never = type ;
366+ assert . fail ( `Unreachable: ${ exhaustive } JavaScript modules are unsupported` ) ;
367+ }
368+
318369const encoder = new TextEncoder ( ) ;
319370const decoder = new TextDecoder ( ) ;
320371export function contentsToString ( contents : string | Uint8Array ) : string {
@@ -331,16 +382,18 @@ export function convertModuleDefinition(
331382 // The runtime requires module identifiers to be relative paths
332383 let name = path . relative ( modulesRoot , def . path ) ;
333384 if ( path . sep === "\\" ) name = name . replaceAll ( "\\" , "/" ) ;
334- let contents = def . contents ?? readFileSync ( def . path ) ;
385+ const contents = def . contents ?? readFileSync ( def . path ) ;
335386 switch ( def . type ) {
336387 case "ESModule" :
337- contents = contentsToString ( contents ) ;
338- contents = sourceMapRegistry . register ( contents , def . path ) ;
339- return { name, esModule : contents } ;
340388 case "CommonJS" :
341- contents = contentsToString ( contents ) ;
342- contents = sourceMapRegistry . register ( contents , def . path ) ;
343- return { name, commonJsModule : contents } ;
389+ case "NodeJsCompatModule" :
390+ return createJavaScriptModule (
391+ sourceMapRegistry ,
392+ contentsToString ( contents ) ,
393+ name ,
394+ def . path ,
395+ def . type
396+ ) ;
344397 case "Text" :
345398 return { name, text : contentsToString ( contents ) } ;
346399 case "Data" :
@@ -349,22 +402,32 @@ export function convertModuleDefinition(
349402 return { name, wasm : contentsToArray ( contents ) } ;
350403 default :
351404 // `type` should've been validated against `ModuleRuleTypeSchema`
352- assert . fail ( `Unreachable: ${ def . type } modules are unsupported` ) ;
405+ const exhaustive : never = def . type ;
406+ assert . fail ( `Unreachable: ${ exhaustive } modules are unsupported` ) ;
353407 }
354408}
355409function convertWorkerModule ( mod : Worker_Module ) : ModuleDefinition {
356410 const path = mod . name ;
357411 assert ( path !== undefined ) ;
358412
359- if ( "esModule" in mod ) return { path, type : "ESModule" } ;
360- else if ( "commonJsModule" in mod ) return { path, type : "CommonJS" } ;
361- else if ( "text" in mod ) return { path, type : "Text" } ;
362- else if ( "data" in mod ) return { path, type : "Data" } ;
363- else if ( "wasm" in mod ) return { path, type : "CompiledWasm" } ;
413+ // Mark keys in `mod` as required for exhaustiveness checking
414+ const m = mod as Required < Worker_Module > ;
415+
416+ if ( "esModule" in m ) return { path, type : "ESModule" } ;
417+ else if ( "commonJsModule" in m ) return { path, type : "CommonJS" } ;
418+ else if ( "nodeJsCompatModule" in m )
419+ return { path, type : "NodeJsCompatModule" } ;
420+ else if ( "text" in m ) return { path, type : "Text" } ;
421+ else if ( "data" in m ) return { path, type : "Data" } ;
422+ else if ( "wasm" in m ) return { path, type : "CompiledWasm" } ;
364423
365424 // This function is only used for building error messages including
366425 // generated modules, and these are the types we generate.
426+ assert ( ! ( "json" in m ) , "Unreachable: json modules aren't generated" ) ;
427+ const exhaustive : never = m ;
367428 assert . fail (
368- `Unreachable: [${ Object . keys ( mod ) . join ( ", " ) } ] modules are unsupported`
429+ `Unreachable: [${ Object . keys ( exhaustive ) . join (
430+ ", "
431+ ) } ] modules are unsupported`
369432 ) ;
370433}
0 commit comments