@@ -6,17 +6,23 @@ import type { TransformOptions } from 'esbuild';
66import { transform , transformSync } from '../../utils/transform/index.js' ;
77import { transformDynamicImport } from '../../utils/transform/transform-dynamic-import.js' ;
88import { inlineSourceMap } from '../../source-map.js' ;
9- import { isFeatureSupported , importAttributes , esmLoadReadFile } from '../../utils/node-features.js' ;
9+ import {
10+ isFeatureSupported ,
11+ importAttributes ,
12+ esmLoadReadFile ,
13+ requireEsm ,
14+ loadReadFromSource ,
15+ } from '../../utils/node-features.js' ;
1016import { parent } from '../../utils/ipc/client.js' ;
1117import type { Message } from '../types.js' ;
1218import { fileMatcher } from '../../utils/tsconfig.js' ;
1319import { isJsonPattern , tsExtensionsPattern , fileUrlPrefix } from '../../utils/path-utils.js' ;
1420import { isESM } from '../../utils/es-module-lexer.js' ;
1521import { logEsm as log , debugEnabled } from '../../utils/debug.js' ;
16- import { getNamespace } from './utils.js' ;
22+ import { getNamespace , decodeCjsQuery } from './utils.js' ;
1723import { data } from './initialize.js' ;
1824
19- const contextAttributesProperty = (
25+ const importAttributesProperty = (
2026 isFeatureSupported ( importAttributes )
2127 ? 'importAttributes'
2228 : 'importAssertions' as 'importAttributes'
@@ -32,6 +38,9 @@ let load: LoadHook = async (
3238 return nextLoad ( url , context ) ;
3339 }
3440
41+ // TODO: Add version
42+ url = decodeCjsQuery ( url ) ;
43+
3544 const urlNamespace = getNamespace ( url ) ;
3645 if ( data . namespace && data . namespace !== urlNamespace ) {
3746 return nextLoad ( url , context ) ;
@@ -57,11 +66,121 @@ let load: LoadHook = async (
5766 } ) ;
5867 }
5968
69+ const filePath = url . startsWith ( fileUrlPrefix ) ? fileURLToPath ( url ) : url ;
70+
71+ if ( context . format === 'module-json' ) {
72+ const code = await readFile ( new URL ( url ) , 'utf8' ) ;
73+ const transformed = await transform (
74+ code ,
75+ filePath ,
76+ {
77+ tsconfigRaw : (
78+ path . isAbsolute ( filePath )
79+ ? fileMatcher ?.( filePath ) as TransformOptions [ 'tsconfigRaw' ]
80+ : undefined
81+ ) ,
82+ } ,
83+ ) ;
84+ return {
85+ shortCircuit : true ,
86+ format : 'module' ,
87+ source : inlineSourceMap ( transformed ) ,
88+ } ;
89+ }
90+
91+ if ( context . format === 'commonjs-json' ) {
92+ const code = await readFile ( new URL ( url ) , 'utf8' ) ;
93+ const transformed = transformSync (
94+ code ,
95+ filePath ,
96+ {
97+ tsconfigRaw : (
98+ path . isAbsolute ( filePath )
99+ ? fileMatcher ?.( filePath ) as TransformOptions [ 'tsconfigRaw' ]
100+ : undefined
101+ ) ,
102+ } ,
103+ ) ;
104+
105+ const jsonExportKeys = Object . keys ( JSON . parse ( code ) ) ;
106+ transformed . code += `\n0 && (module.exports = {${ jsonExportKeys . join ( ',' ) } });` ;
107+
108+ return {
109+ shortCircuit : true ,
110+ format : 'commonjs' ,
111+ source : inlineSourceMap ( transformed ) ,
112+ } ;
113+ }
114+
115+ /**
116+ * For compiling ambiguous ESM (ESM syntax in a package without type)
117+ * So Node.js can kind of do this now, but it tries CommonJS first, and if it fails,
118+ * it uses NATIVE ESM. This means ESM code that uses mixed syntax (e.g. __dirname)
119+ * wil not work.
120+ */
121+ if (
122+ (
123+ context . format === undefined
124+ || context . format === 'commonjs'
125+ || context . format === 'commonjs-typescript'
126+ )
127+ && isFeatureSupported ( esmLoadReadFile )
128+ && url . startsWith ( 'file:' ) // Could be data:
129+ && filePath . endsWith ( '.js' )
130+ ) {
131+ const code = await readFile ( new URL ( url ) , 'utf8' ) ;
132+
133+ // if the file extension is .js, only transform if using esm syntax
134+ if ( isESM ( code ) ) {
135+ /**
136+ * es or cjs module lexer unfortunately cannot be used because it doesn't support
137+ * typescript syntax
138+ *
139+ * While the full code is transformed, only the exports are used for parsing.
140+ * In fact, the code can't even run because imports cannot be resolved relative
141+ * from the data: URL.
142+ *
143+ * This should pre-compile for the CJS loader to have a cache hit
144+ *
145+ * I considered extracting the CJS exports from esbuild via (0&&(module.exports={})
146+ * to minimize the data URL size but this only works for ESM->CJS and not CTS files
147+ * which are already in CJS syntax.
148+ * In CTS, module.exports can be written in any pattern.
149+ */
150+ const transformed = transformSync (
151+ code ,
152+ url ,
153+ {
154+ tsconfigRaw : fileMatcher ?.( filePath ) as TransformOptions [ 'tsconfigRaw' ] ,
155+ } ,
156+ ) ;
157+
158+ if ( isFeatureSupported ( loadReadFromSource ) ) {
159+ return {
160+ shortCircuit : true ,
161+ format : 'commonjs' ,
162+
163+ // This is necessary for CJS exports to be parsed correctly
164+ // Returning a `source` makes the ESM translator to handle
165+ // the CJS compilation and skips the CJS loader
166+ source : inlineSourceMap ( transformed ) ,
167+ } ;
168+ }
169+
170+ const filePathWithNamespace = urlNamespace ? `${ filePath } ?namespace=${ encodeURIComponent ( urlNamespace ) } ` : filePath ;
171+ return {
172+ shortCircuit : true ,
173+ format : 'commonjs' ,
174+ responseURL : `data:text/javascript,${ encodeURIComponent ( transformed . code ) } ?filePath=${ encodeURIComponent ( filePathWithNamespace ) } ` ,
175+ } ;
176+ }
177+ }
178+
60179 if ( isJsonPattern . test ( url ) ) {
61- let contextAttributes = context [ contextAttributesProperty ] ;
180+ let contextAttributes = context [ importAttributesProperty ] ;
62181 if ( ! contextAttributes ) {
63182 contextAttributes = { } ;
64- context [ contextAttributesProperty ] = contextAttributes ;
183+ context [ importAttributesProperty ] = contextAttributes ;
65184 }
66185
67186 if ( ! contextAttributes . type ) {
@@ -75,7 +194,21 @@ let load: LoadHook = async (
75194 loaded,
76195 } ) ;
77196
78- const filePath = url . startsWith ( fileUrlPrefix ) ? fileURLToPath ( url ) : url ;
197+ if (
198+ isFeatureSupported ( loadReadFromSource )
199+ && loaded . format === 'commonjs'
200+ && filePath . endsWith ( '.cjs' )
201+ ) {
202+ let code = await readFile ( new URL ( url ) , 'utf8' ) ;
203+ // Contains native ESM check
204+ const transformed = transformDynamicImport ( filePath , code ) ;
205+ if ( transformed ) {
206+ code = inlineSourceMap ( transformed ) ;
207+ }
208+ loaded . source = code ;
209+ loaded . shortCircuit = true ;
210+ return loaded ;
211+ }
79212
80213 if (
81214 loaded . format === 'commonjs'
@@ -86,7 +219,13 @@ let load: LoadHook = async (
86219 const code = await readFile ( new URL ( url ) , 'utf8' ) ;
87220
88221 // if the file extension is .js, only transform if using esm syntax
89- if ( ! filePath . endsWith ( '.js' ) || isESM ( code ) ) {
222+ if (
223+ // TypeScript files
224+ ! filePath . endsWith ( '.js' )
225+
226+ // ESM syntax in CommonJS type package
227+ || isESM ( code )
228+ ) {
90229 /**
91230 * es or cjs module lexer unfortunately cannot be used because it doesn't support
92231 * typescript syntax
@@ -104,15 +243,26 @@ let load: LoadHook = async (
104243 */
105244 const transformed = transformSync (
106245 code ,
107- filePath ,
246+ url ,
108247 {
109248 tsconfigRaw : fileMatcher ?.( filePath ) as TransformOptions [ 'tsconfigRaw' ] ,
110249 } ,
111250 ) ;
112251
113- const filePathWithNamespace = urlNamespace ? `${ filePath } ?namespace=${ encodeURIComponent ( urlNamespace ) } ` : filePath ;
114-
115- loaded . responseURL = `data:text/javascript,${ encodeURIComponent ( transformed . code ) } ?filePath=${ encodeURIComponent ( filePathWithNamespace ) } ` ;
252+ if ( isFeatureSupported ( loadReadFromSource ) ) {
253+ /**
254+ * Compile ESM to CJS
255+ * In v22.15, the CJS loader logic is now moved to the ESM loader
256+ */
257+ loaded . source = inlineSourceMap ( transformed ) ;
258+ } else {
259+ /**
260+ * This tricks Node into thinking the file is a data URL so it doesn't try to read from disk
261+ * to parse the CJS exports
262+ */
263+ const filePathWithNamespace = urlNamespace ? `${ filePath } ?namespace=${ encodeURIComponent ( urlNamespace ) } ` : filePath ;
264+ loaded . responseURL = `data:text/javascript,${ encodeURIComponent ( transformed . code ) } ?filePath=${ encodeURIComponent ( filePathWithNamespace ) } ` ;
265+ }
116266
117267 log ( 'returning CJS export annotation' , loaded ) ;
118268 return loaded ;
@@ -126,8 +276,40 @@ let load: LoadHook = async (
126276
127277 const code = loaded . source . toString ( ) ;
128278
279+ // Since CJS can now require ESM, JSONs are now handled by the
280+ // ESM loader as ESM in module contexts
281+ // TODO: If we can detect whether this was "required",
282+ // we can let the CJS loader handler it by returning an empty source
283+ // Support named imports in JSON modules
284+ /**
285+ * In versions of Node that supports require'ing ESM,
286+ */
287+ if (
288+ isFeatureSupported ( requireEsm )
289+ && loaded . format === 'json'
290+ ) {
291+ const transformed = transformSync (
292+ code ,
293+ filePath ,
294+ {
295+ tsconfigRaw : (
296+ path . isAbsolute ( filePath )
297+ ? fileMatcher ?.( filePath ) as TransformOptions [ 'tsconfigRaw' ]
298+ : undefined
299+ ) ,
300+ } ,
301+ ) ;
302+
303+ const jsonExportKeys = Object . keys ( JSON . parse ( code ) ) ;
304+ transformed . code += `\n0 && (module.exports = {${ jsonExportKeys . join ( ',' ) } });` ;
305+
306+ return {
307+ format : 'commonjs' ,
308+ source : inlineSourceMap ( transformed ) ,
309+ } ;
310+ }
311+
129312 if (
130- // Support named imports in JSON modules
131313 loaded . format === 'json'
132314 || tsExtensionsPattern . test ( url )
133315 ) {
0 commit comments