diff --git a/src/cjs/index.ts b/src/cjs/index.ts index f2ab5fe37..e119ec7d6 100644 --- a/src/cjs/index.ts +++ b/src/cjs/index.ts @@ -11,6 +11,7 @@ import type { TransformOptions } from 'esbuild'; import { installSourceMapSupport } from '../source-map'; import { transformSync, transformDynamicImport } from '../utils/transform'; import { resolveTsPath } from '../utils/resolve-ts-path'; +import { isESM } from '../utils/esm-pattern'; const isRelativePathPattern = /^\.{1,2}\//; const isTsFilePatten = /\.[cm]?tsx?$/; @@ -33,29 +34,25 @@ const applySourceMap = installSourceMapSupport(); const extensions = Module._extensions; const defaultLoader = extensions['.js']; -const transformExtensions = [ - '.js', - '.cjs', +const typescriptExtensions = [ '.cts', - '.mjs', '.mts', '.ts', '.tsx', '.jsx', ]; +const transformExtensions = [ + '.js', + '.cjs', + '.mjs', +]; + const transformer = ( module: Module, filePath: string, ) => { - const shouldTransformFile = transformExtensions.some(extension => filePath.endsWith(extension)); - if (!shouldTransformFile) { - return defaultLoader(module, filePath); - } - - /** - * For tracking dependencies in watch mode - */ + // For tracking dependencies in watch mode if (process.send) { process.send({ type: 'dependency', @@ -63,14 +60,25 @@ const transformer = ( }); } - let code = fs.readFileSync(filePath, 'utf8'); + const transformTs = typescriptExtensions.some(extension => filePath.endsWith(extension)); + const transformJs = transformExtensions.some(extension => filePath.endsWith(extension)); + if (!transformTs && !transformJs) { + return defaultLoader(module, filePath); + } + let code = fs.readFileSync(filePath, 'utf8'); if (filePath.endsWith('.cjs')) { + // Contains native ESM check const transformed = transformDynamicImport(filePath, code); if (transformed) { code = applySourceMap(transformed, filePath); } - } else { + } else if ( + transformTs + + // CommonJS file but uses ESM import/export + || isESM(code) + ) { const transformed = transformSync( code, filePath, diff --git a/src/esm/loaders.ts b/src/esm/loaders.ts index 6bde3ea38..d32239ef5 100644 --- a/src/esm/loaders.ts +++ b/src/esm/loaders.ts @@ -248,6 +248,10 @@ export const load: LoadHook = async function ( context, defaultLoad, ) { + /* + Filter out node:* + Maybe only handle files that start with file:// + */ if (sendToParent) { sendToParent({ type: 'dependency', @@ -264,6 +268,7 @@ export const load: LoadHook = async function ( const loaded = await defaultLoad(url, context); + // CommonJS and Internal modules (e.g. node:*) if (!loaded.source) { return loaded; } diff --git a/src/utils/debug.ts b/src/utils/debug.ts new file mode 100644 index 000000000..4fb70d6a4 --- /dev/null +++ b/src/utils/debug.ts @@ -0,0 +1,36 @@ +export const time = ( + name: string, + _function: (...args: Argument[]) => unknown, +) => function ( + this: unknown, + ...args: Argument[] + ) { + const timeStart = Date.now(); + const logTimeElapsed = () => { + const elapsed = Date.now() - timeStart; + + if (elapsed > 10) { + // console.log({ + // name, + // args, + // elapsed, + // }); + } + }; + + const result = Reflect.apply(_function, this, args); + if ( + result + && typeof result === 'object' + && 'then' in result + ) { + (result as Promise).then( + logTimeElapsed, + // Ignore error in this chain + () => {}, + ); + } else { + logTimeElapsed(); + } + return result; + }; diff --git a/src/utils/esm-pattern.ts b/src/utils/esm-pattern.ts new file mode 100644 index 000000000..03541e1a1 --- /dev/null +++ b/src/utils/esm-pattern.ts @@ -0,0 +1,24 @@ +import { parseEsm } from './es-module-lexer'; +/* +TODO: Add tests +Catches: +import a from 'b' +import 'b'; +import('b'); +export{a}; +export default a; + +Doesn't catch: +EXPORT{a} +exports.a = 1 +module.exports = 1 + */ +const esmPattern = /\b(?:import|export)\b/; + +export const isESM = (code: string) => { + if (esmPattern.test(code)) { + const [imports, exports] = parseEsm(code); + return imports.length > 0 || exports.length > 0; + } + return false; +}; diff --git a/src/utils/transform/transform-dynamic-import.ts b/src/utils/transform/transform-dynamic-import.ts index 7bc18bf17..682d56db2 100644 --- a/src/utils/transform/transform-dynamic-import.ts +++ b/src/utils/transform/transform-dynamic-import.ts @@ -2,25 +2,33 @@ import MagicString from 'magic-string'; import type { RawSourceMap } from '../../source-map'; import { parseEsm } from '../es-module-lexer'; -const checkEsModule = `.then((mod)=>{ - const exports = Object.keys(mod); - if( - exports.length===1&&exports[0]==='default'&&mod.default&&mod.default.__esModule - ){ - return mod.default +const handlerName = '___tsxInteropDynamicImport'; +const handleEsModuleFunction = `function ${handlerName}${(function (imported: Record) { + const d = 'default'; + const exports = Object.keys(imported); + if ( + exports.length === 1 + && exports[0] === d + && imported[d] + && typeof imported[d] === 'object' + && '__esModule' in imported[d] + ) { + return imported[d]; } - return mod -})` - // replaceAll is not supported in Node 12 - // eslint-disable-next-line unicorn/prefer-string-replace-all - .replace(/[\n\t]+/g, ''); -export function transformDynamicImport( + return imported; +}).toString().slice('function'.length)}`; + +const handleDynamicImport = `.then(${handlerName})`; + +const esmImportPattern = /\bimport\b/; + +export const transformDynamicImport = ( filePath: string, code: string, -) { +) => { // Naive check - if (!code.includes('import')) { + if (!esmImportPattern.test(code)) { return; } @@ -33,14 +41,25 @@ export function transformDynamicImport( const magicString = new MagicString(code); for (const dynamicImport of dynamicImports) { - magicString.appendRight(dynamicImport.se, checkEsModule); + magicString.appendRight(dynamicImport.se, handleDynamicImport); } + magicString.append(handleEsModuleFunction); + + const newCode = magicString.toString(); + const newMap = magicString.generateMap({ + source: filePath, + includeContent: false, + + /** + * The performance hit on this is very high + * Since we're only transforming import()s, I think this may be overkill + */ + // hires: 'boundary', + }) as unknown as RawSourceMap; + return { - code: magicString.toString(), - map: magicString.generateMap({ - source: filePath, - hires: true, - }) as unknown as RawSourceMap, + code: newCode, + map: newMap, }; -} +};