diff --git a/docs/node/tsx-require.md b/docs/node/tsx-require.md index fabec1f6..147d672f 100644 --- a/docs/node/tsx-require.md +++ b/docs/node/tsx-require.md @@ -7,7 +7,7 @@ Use this function for importing TypeScript files in CommonJS mode without adding Note, the current file path must be passed in as the second argument to resolve the import context. ::: warning Caveats -- `import()` & asynchronous `require()` calls in the loaded files are not enhanced. +- `import()` calls in the loaded files are not enhanced. - Because it compiles ESM syntax to run in CommonJS mode, top-level await is not supported ::: diff --git a/src/cjs/api/module-extensions.ts b/src/cjs/api/module-extensions.ts index ab72b59a..04011e42 100644 --- a/src/cjs/api/module-extensions.ts +++ b/src/cjs/api/module-extensions.ts @@ -22,103 +22,117 @@ const transformExtensions = [ '.mjs', ] as const; -// Clone Module._extensions with null prototype -export const extensions: NodeJS.RequireExtensions = Object.assign( - Object.create(null), - Module._extensions, -); - -const defaultLoader = extensions['.js']; - -const transformer = ( - module: Module, - filePath: string, +export const createExtensions = ( + extendExtensions: NodeJS.RequireExtensions, + namespace?: string, ) => { - // Make sure __filename doesnt contain query - const cleanFilePath = filePath.split('?')[0]; - - // For tracking dependencies in watch mode - if (parent?.send) { - parent.send({ - type: 'dependency', - path: cleanFilePath, - }); - } + // Clone Module._extensions with null prototype + const extensions: NodeJS.RequireExtensions = Object.assign( + Object.create(null), + extendExtensions, + ); + + const defaultLoader = extensions['.js']; + + const transformer = ( + module: Module, + filePath: string, + ) => { + // Make sure __filename doesnt contain query + const [cleanFilePath, query] = filePath.split('?'); + + const searchParams = new URLSearchParams(query); + + // If request namespace doesnt match the namespace, ignore + if ((searchParams.get('namespace') ?? undefined) !== namespace) { + return defaultLoader(module, cleanFilePath); + } - const transformTs = typescriptExtensions.some(extension => cleanFilePath.endsWith(extension)); - const transformJs = transformExtensions.some(extension => cleanFilePath.endsWith(extension)); - if (!transformTs && !transformJs) { - return defaultLoader(module, cleanFilePath); - } + // For tracking dependencies in watch mode + if (parent?.send) { + parent.send({ + type: 'dependency', + path: cleanFilePath, + }); + } - let code = fs.readFileSync(cleanFilePath, 'utf8'); + const transformTs = typescriptExtensions.some(extension => cleanFilePath.endsWith(extension)); + const transformJs = transformExtensions.some(extension => cleanFilePath.endsWith(extension)); + if (!transformTs && !transformJs) { + return defaultLoader(module, cleanFilePath); + } + + let code = fs.readFileSync(cleanFilePath, 'utf8'); + + if (cleanFilePath.endsWith('.cjs')) { + // Contains native ESM check + const transformed = transformDynamicImport(filePath, code); + if (transformed) { + code = ( + shouldApplySourceMap() + ? inlineSourceMap(transformed) + : transformed.code + ); + } + } else if ( + transformTs + + // CommonJS file but uses ESM import/export + || isESM(code) + ) { + const transformed = transformSync( + code, + filePath, + { + tsconfigRaw: fileMatcher?.(cleanFilePath) as TransformOptions['tsconfigRaw'], + }, + ); - if (cleanFilePath.endsWith('.cjs')) { - // Contains native ESM check - const transformed = transformDynamicImport(filePath, code); - if (transformed) { code = ( shouldApplySourceMap() ? inlineSourceMap(transformed) : transformed.code ); } - } else if ( - transformTs - - // CommonJS file but uses ESM import/export - || isESM(code) - ) { - const transformed = transformSync( - code, - filePath, - { - tsconfigRaw: fileMatcher?.(cleanFilePath) as TransformOptions['tsconfigRaw'], - }, - ); - - code = ( - shouldApplySourceMap() - ? inlineSourceMap(transformed) - : transformed.code - ); - } - - module._compile(code, cleanFilePath); -}; -/** - * Handles .cjs, .cts, .mts & any explicitly specified extension that doesn't match any loaders - * - * Any file requested with an explicit extension will be loaded using the .js loader: - * https://github.com/nodejs/node/blob/e339e9c5d71b72fd09e6abd38b10678e0c592ae7/lib/internal/modules/cjs/loader.js#L430 - */ -extensions['.js'] = transformer; - -[ - '.ts', - '.tsx', - '.jsx', + module._compile(code, cleanFilePath); + }; /** - * Loaders for extensions .cjs, .cts, & .mts don't need to be - * registered because they're explicitly specified. And unknown - * extensions (incl .cjs) fallsback to using the '.js' loader: - * https://github.com/nodejs/node/blob/v18.4.0/lib/internal/modules/cjs/loader.js#L430 + * Handles .cjs, .cts, .mts & any explicitly specified extension that doesn't match any loaders * - * That said, it's actually ".js" and ".mjs" that get special treatment - * rather than ".cjs" (it might as well be ".random-ext") + * Any file requested with an explicit extension will be loaded using the .js loader: + * https://github.com/nodejs/node/blob/e339e9c5d71b72fd09e6abd38b10678e0c592ae7/lib/internal/modules/cjs/loader.js#L430 */ - '.mjs', -].forEach((extension) => { - Object.defineProperty(extensions, extension, { - value: transformer, + extensions['.js'] = transformer; + + [ + '.ts', + '.tsx', + '.jsx', /** - * Prevent Object.keys from detecting these extensions - * when CJS loader iterates over the possible extensions - * https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/cjs/loader.js#L609 + * Loaders for extensions .cjs, .cts, & .mts don't need to be + * registered because they're explicitly specified. And unknown + * extensions (incl .cjs) fallsback to using the '.js' loader: + * https://github.com/nodejs/node/blob/v18.4.0/lib/internal/modules/cjs/loader.js#L430 + * + * That said, it's actually ".js" and ".mjs" that get special treatment + * rather than ".cjs" (it might as well be ".random-ext") */ - enumerable: false, + '.mjs', + ].forEach((extension) => { + Object.defineProperty(extensions, extension, { + value: transformer, + + /** + * Prevent Object.keys from detecting these extensions + * when CJS loader iterates over the possible extensions + * https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/cjs/loader.js#L609 + */ + enumerable: false, + }); }); -}); + + return extensions; +}; diff --git a/src/cjs/api/module-resolve-filename.ts b/src/cjs/api/module-resolve-filename.ts index 4fa3938c..a801f3ae 100644 --- a/src/cjs/api/module-resolve-filename.ts +++ b/src/cjs/api/module-resolve-filename.ts @@ -5,6 +5,7 @@ import { resolveTsPath } from '../../utils/resolve-ts-path.js'; import type { NodeError } from '../../types.js'; import { isRelativePath, fileUrlPrefix, tsExtensionsPattern } from '../../utils/path-utils.js'; import { tsconfigPathsMatcher, allowJs } from '../../utils/tsconfig.js'; +import { urlSearchParamsStringify } from '../../utils/url-search-params-stringify.js'; type ResolveFilename = typeof Module._resolveFilename; @@ -87,33 +88,50 @@ const tryExtensions = ( export const createResolveFilename = ( nextResolve: ResolveFilename, + namespace?: string, ): ResolveFilename => ( request, parent, isMain, options, ) => { + const resolve: SimpleResolve = request_ => nextResolve( + request_, + parent, + isMain, + options, + ); + request = interopCjsExports(request); // Strip query string - const queryIndex = request.indexOf('?'); - const query = queryIndex === -1 ? '' : request.slice(queryIndex); - if (queryIndex !== -1) { - request = request.slice(0, queryIndex); + const [cleanRequest, queryString] = request.split('?'); + const searchParams = new URLSearchParams(queryString); + + // Inherit parent namespace if it exists + if (parent?.filename) { + const parentQuery = new URLSearchParams(parent.filename.split('?')[1]); + const parentNamespace = parentQuery.get('namespace'); + if (parentNamespace) { + searchParams.append('namespace', parentNamespace); + } + } + + // If request namespace doesnt match the namespace, ignore + if ((searchParams.get('namespace') ?? undefined) !== namespace) { + return resolve(request); } + const query = urlSearchParamsStringify(searchParams); + + // Temporarily remove query since default resolver can't handle it. Added back later. + request = cleanRequest; + // Support file protocol if (request.startsWith(fileUrlPrefix)) { request = fileURLToPath(request); } - const resolve: SimpleResolve = request_ => nextResolve( - request_, - parent, - isMain, - options, - ); - // Resolve TS path alias if ( tsconfigPathsMatcher @@ -157,7 +175,10 @@ export const createResolveFilename = ( } try { - return resolve(request) + query; + const resolved = resolve(request); + + // Can be a node core module + return resolved + (path.isAbsolute(resolved) ? query : ''); } catch (error) { const resolved = ( tryExtensions(resolve, request) diff --git a/src/cjs/api/register.ts b/src/cjs/api/register.ts index 247ef0dc..6c2c7af1 100644 --- a/src/cjs/api/register.ts +++ b/src/cjs/api/register.ts @@ -1,9 +1,61 @@ import Module from 'node:module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { loadTsconfig } from '../../utils/tsconfig.js'; -import { extensions } from './module-extensions.js'; +import type { RequiredProperty } from '../../types.js'; +import { urlSearchParamsStringify } from '../../utils/url-search-params-stringify.js'; +import { createExtensions } from './module-extensions.js'; import { createResolveFilename } from './module-resolve-filename.js'; -export const register = () => { +const resolveContext = ( + id: string, + fromFile: string | URL, +) => { + if (!fromFile) { + throw new Error('The current file path (__filename or import.meta.url) must be provided in the second argument of tsx.require()'); + } + + if ( + (typeof fromFile === 'string' && fromFile.startsWith('file://')) + || fromFile instanceof URL + ) { + fromFile = fileURLToPath(fromFile); + } + + return path.resolve(path.dirname(fromFile), id); +}; + +type RegisterOptions = { + namespace?: string; +}; + +export type Unregister = () => void; + +type ScopedRequire = ( + id: string, + fromFile: string | URL, +) => any; // eslint-disable-line @typescript-eslint/no-explicit-any + +type ScopedResolve = ( + id: string, + fromFile: string | URL, + resolveOptions?: { paths?: string[] | undefined }, +) => string; + +export type NamespacedUnregister = Unregister & { + require: ScopedRequire; + resolve: ScopedResolve; + unregister: Unregister; +}; + +export type Register = { + (options: RequiredProperty): NamespacedUnregister; + (options?: RegisterOptions): Unregister; +}; + +export const register: Register = ( + options, +) => { const { sourceMapsEnabled } = process; const { _extensions, _resolveFilename } = Module; @@ -11,8 +63,10 @@ export const register = () => { // register process.setSourceMapsEnabled(true); - const resolveFilename = createResolveFilename(_resolveFilename); + const resolveFilename = createResolveFilename(_resolveFilename, options?.namespace); Module._resolveFilename = resolveFilename; + + const extensions = createExtensions(Module._extensions, options?.namespace); // @ts-expect-error overwriting read-only property Module._extensions = extensions; @@ -26,5 +80,40 @@ export const register = () => { Module._resolveFilename = _resolveFilename; }; + if (options?.namespace) { + const scopedRequire: ScopedRequire = (id, fromFile) => { + const resolvedId = resolveContext(id, fromFile); + const [request, query] = resolvedId.split('?'); + + const parameters = new URLSearchParams(query); + if (options.namespace) { + parameters.set('namespace', options.namespace); + } + + // eslint-disable-next-line n/global-require,import-x/no-dynamic-require + return require(request + urlSearchParamsStringify(parameters)); + }; + unregister.require = scopedRequire; + + const scopedResolve: ScopedResolve = (id, fromFile, resolveOptions) => { + const resolvedId = resolveContext(id, fromFile); + const [request, query] = resolvedId.split('?'); + + const parameters = new URLSearchParams(query); + if (options.namespace) { + parameters.set('namespace', options.namespace); + } + + return resolveFilename( + request + urlSearchParamsStringify(parameters), + module, + false, + resolveOptions, + ); + }; + unregister.resolve = scopedResolve; + unregister.unregister = unregister; + } + return unregister; }; diff --git a/src/cjs/api/require.ts b/src/cjs/api/require.ts index e112c76c..ada27db4 100644 --- a/src/cjs/api/require.ts +++ b/src/cjs/api/require.ts @@ -1,50 +1,29 @@ -import Module from 'node:module'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { register } from './register.js'; -import { createResolveFilename } from './module-resolve-filename.js'; - -const getRequestContext = ( - id: string, - fromFile: string | URL, -) => { - if (!fromFile) { - throw new Error('The current file path (__filename or import.meta.url) must be provided in the second argument of tsx.require()'); - } - - if ( - (typeof fromFile === 'string' && fromFile.startsWith('file://')) - || fromFile instanceof URL - ) { - fromFile = fileURLToPath(fromFile); - } - - return path.resolve(path.dirname(fromFile), id); -}; +import { register, type NamespacedUnregister } from './register.js'; +let api: NamespacedUnregister | undefined; const tsxRequire = ( id: string, fromFile: string | URL, ) => { - const contextId = getRequestContext(id, fromFile); - const unregister = register(); - try { - // eslint-disable-next-line import-x/no-dynamic-require, n/global-require - return require(contextId); - } finally { - unregister(); + if (!api) { + api = register({ + namespace: Date.now().toString(), + }); } + return api.require(id, fromFile); }; -const resolveFilename = createResolveFilename(Module._resolveFilename); - const resolve = ( id: string, fromFile: string | URL, options?: { paths?: string[] | undefined }, ) => { - const contextId = getRequestContext(id, fromFile); - return resolveFilename(contextId, module, false, options); + if (!api) { + api = register({ + namespace: Date.now().toString(), + }); + } + return api.resolve(id, fromFile, options); }; resolve.paths = require.resolve.paths; diff --git a/src/esm/api/register.ts b/src/esm/api/register.ts index fe7233f4..f5bf2b13 100644 --- a/src/esm/api/register.ts +++ b/src/esm/api/register.ts @@ -1,6 +1,7 @@ import module from 'node:module'; import { MessageChannel, type MessagePort } from 'node:worker_threads'; import type { Message } from '../types.js'; +import type { RequiredProperty } from '../../types.js'; import { interopCjsExports } from '../../cjs/api/module-resolve-filename.js'; import { createScopedImport, type ScopedImport } from './scoped-import.js'; @@ -25,8 +26,6 @@ export type NamespacedUnregister = Unregister & { unregister: Unregister; }; -type RequiredProperty = Type & { [P in Keys]-?: Type[P] }; - export type Register = { (options: RequiredProperty): NamespacedUnregister; (options?: RegisterOptions): Unregister; diff --git a/src/esm/hook/utils.ts b/src/esm/hook/utils.ts index a610f59b..d1e44b30 100644 --- a/src/esm/hook/utils.ts +++ b/src/esm/hook/utils.ts @@ -4,12 +4,8 @@ import { tsExtensionsPattern } from '../../utils/path-utils.js'; import { getPackageType } from './package-json.js'; const getFormatFromExtension = (fileUrl: string): ModuleFormat | undefined => { - const queryIndex = fileUrl.indexOf('?'); - fileUrl = ( - queryIndex === -1 - ? fileUrl - : fileUrl.slice(0, queryIndex) - ); + [fileUrl] = fileUrl.split('?'); + const extension = path.extname(fileUrl); if (extension === '.json') { return 'json'; diff --git a/src/types.ts b/src/types.ts index 64d58ee7..1b300a8c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ export type NodeError = Error & { code: string; }; + +export type RequiredProperty = Type & { [P in Keys]-?: Type[P] }; diff --git a/src/utils/url-search-params-stringify.ts b/src/utils/url-search-params-stringify.ts new file mode 100644 index 00000000..419ac167 --- /dev/null +++ b/src/utils/url-search-params-stringify.ts @@ -0,0 +1,7 @@ +export const urlSearchParamsStringify = ( + searchParams: URLSearchParams, +) => { + // URLSearchParams#size not implemented in Node 18.0.0 + const size = Array.from(searchParams).length; + return size > 0 ? `?${searchParams.toString()}` : ''; +}; diff --git a/tests/specs/api.ts b/tests/specs/api.ts index 83fa061f..e7b752b7 100644 --- a/tests/specs/api.ts +++ b/tests/specs/api.ts @@ -9,18 +9,22 @@ import { tsxEsmApiCjsPath, type NodeApis, } from '../utils/tsx.js'; -import { createPackageJson, createTsconfig } from '../fixtures.js'; +import { createPackageJson, createTsconfig, expectErrors } from '../fixtures.js'; const tsFiles = { 'file.ts': ` import { foo } from './foo' - export const message = foo as string + export const message = \`\${foo} \${(typeof __filename === 'undefined' ? import.meta.url : __filename).split(/[\\\\/]/).pop()}\` as string + export { async } from './foo' `, 'foo.ts': ` + import { setTimeout } from 'node:timers/promises' import { bar } from './bar.js' export const foo = \`foo \${bar}\` as string + export const async = setTimeout(10).then(() => require('./async')).catch((error) => error); `, 'bar.ts': 'export type A = 1; export { bar } from "pkg"', + 'async.ts': 'export default "async"', 'node_modules/pkg': { 'package.json': createPackageJson({ name: 'pkg', @@ -29,6 +33,7 @@ const tsFiles = { }), 'index.js': 'import "node:process"; export const bar = "bar";', }, + ...expectErrors, }; export default testSuite(({ describe }, node: NodeApis) => { @@ -83,7 +88,7 @@ export default testSuite(({ describe }, node: NodeApis) => { nodeOptions: [], }); - expect(stdout).toBe('Fails as expected\nfoo bar\nUnregistered'); + expect(stdout).toBe('Fails as expected\nfoo bar file.ts\nUnregistered'); }); describe('tsx.require()', ({ test }) => { @@ -120,16 +125,16 @@ export default testSuite(({ describe }, node: NodeApis) => { nodeOptions: [], }); - expect(stdout).toBe('Fails as expected\nfoo bar\nfile.ts\nUnpolluted global require'); + expect(stdout).toMatch(/Fails as expected\nfoo bar file.ts\nfile.ts\?namespace=\d+\nUnpolluted global require/); }); test('catchable', async () => { await using fixture = await createFixture({ 'require.cjs': ` const tsx = require(${JSON.stringify(tsxCjsApiPath)}); - try { tsx.require('./file', __filename); } catch {} + try { tsx.require('./syntax-error', __filename); } catch {} `, - 'file.ts': 'if', + 'syntax-error.ts': 'if', }); const { all } = await execaNode(fixture.getPath('require.cjs'), [], { @@ -139,6 +144,77 @@ export default testSuite(({ describe }, node: NodeApis) => { }); expect(all).toBe(''); }); + + test('chainable', async () => { + await using fixture = await createFixture({ + 'require.cjs': ` + const path = require('node:path'); + const tsx = require(${JSON.stringify(tsxCjsApiPath)}); + + const unregister = tsx.register(); + console.log(require('./file').message); + delete require.cache[require.resolve('./file')]; + + const loaded = tsx.require('./file', __filename); + console.log(loaded.message); + + // Remove from cache + const loadedPath = tsx.require.resolve('./file', __filename); + delete require.cache[loadedPath]; + + console.log(require('./file').message); + delete require.cache[require.resolve('./file')]; + + unregister(); + + try { + require('./file'); + } catch { + console.log('Unregistered'); + } + `, + ...tsFiles, + }); + + const { stdout } = await execaNode(fixture.getPath('require.cjs'), [], { + nodePath: node.path, + nodeOptions: [], + }); + + expect(stdout).toBe('foo bar file.ts\nfoo bar file.ts\nfoo bar file.ts\nUnregistered'); + }); + + test('namespace', async () => { + await using fixture = await createFixture({ + 'require.cjs': ` + const { expectErrors } = require('expect-errors'); + const path = require('node:path'); + const tsx = require(${JSON.stringify(tsxCjsApiPath)}); + + const api = tsx.register({ namespace: 'abcd' }); + + expectErrors( + // Loading explicit/resolved file path should be ignored by loader (extensions) + [() => require('./file.ts'), 'SyntaxError'], + + // resolver should preserve full file path when ignoring + [() => require('./file.ts?asdf'), "Cannot find module './file.ts?asdf'"] + ); + + const { message, async } = api.require('./file', __filename); + console.log(message); + async.then(m => console.log(m.default)); + `, + ...tsFiles, + }); + + const { stdout } = await execaNode(fixture.getPath('require.cjs'), [], { + nodePath: node.path, + nodeOptions: [], + }); + + expect(stdout).toBe('foo bar file.ts\nasync'); + }); }); }); @@ -185,7 +261,7 @@ export default testSuite(({ describe }, node: NodeApis) => { nodeOptions: [], }); - expect(stdout).toBe('Fails as expected\nfoo bar'); + expect(stdout).toBe('Fails as expected\nfoo bar file.ts?nocache'); }); describe('register / unregister', ({ test, describe }) => { @@ -199,7 +275,7 @@ export default testSuite(({ describe }, node: NodeApis) => { } catch { console.log('Fails as expected 1'); } - + { const unregister = register(); @@ -231,7 +307,7 @@ export default testSuite(({ describe }, node: NodeApis) => { nodePath: node.path, nodeOptions: [], }); - expect(stdout).toBe('Fails as expected 1\nfoo bar\nFails as expected 2\nfoo bar'); + expect(stdout).toBe('Fails as expected 1\nfoo bar file.ts?2\nFails as expected 2\nfoo bar file.ts?4'); }); test('onImport', async () => { @@ -245,7 +321,7 @@ export default testSuite(({ describe }, node: NodeApis) => { console.log(file.split('/').pop()); }, }); - + await import('./file'); `, ...tsFiles, @@ -255,7 +331,7 @@ export default testSuite(({ describe }, node: NodeApis) => { nodePath: node.path, nodeOptions: [], }); - expect(stdout).toBe('file.ts\nfoo.ts\nbar.ts\nindex.js\nnode:process'); + expect(stdout).toBe('file.ts\nfoo.ts\npromises\nbar.ts\nindex.js\nnode:process'); }); test('namespace & onImport', async () => { @@ -423,17 +499,17 @@ export default testSuite(({ describe }, node: NodeApis) => { 'package.json': createPackageJson({ type: 'module' }), 'import.mjs': ` import { tsImport } from ${JSON.stringify(tsxEsmApiPath)}; - + await import('./file.ts').catch((error) => { console.log('Fails as expected 1'); }); - + const { message } = await tsImport('./file.ts', import.meta.url); console.log(message); - + const { message: message2 } = await tsImport('./file.ts?with-query', import.meta.url); console.log(message2); - + // Global not polluted await import('./file.ts?nocache').catch((error) => { console.log('Fails as expected 2'); @@ -446,7 +522,7 @@ export default testSuite(({ describe }, node: NodeApis) => { nodePath: node.path, nodeOptions: [], }); - expect(stdout).toBe('Fails as expected 1\nfoo bar\nfoo bar\nFails as expected 2'); + expect(stdout).toMatch(/Fails as expected 1\nfoo bar file\.ts\?tsx-namespace=\d+\nfoo bar file\.ts\?with-query=&tsx-namespace=\d+\nFails as expected 2/); }); test('commonjs', async () => { @@ -454,18 +530,18 @@ export default testSuite(({ describe }, node: NodeApis) => { 'package.json': createPackageJson({ type: 'module' }), 'import.cjs': ` const { tsImport } = require(${JSON.stringify(tsxEsmApiCjsPath)}); - + (async () => { await import('./file.ts').catch((error) => { console.log('Fails as expected 1'); }); - + const { message } = await tsImport('./file.ts', __filename); console.log(message); - + const { message: message2 } = await tsImport('./file.ts?with-query', __filename); console.log(message2); - + // Global not polluted await import('./file.ts?nocache').catch((error) => { console.log('Fails as expected 2'); @@ -479,7 +555,7 @@ export default testSuite(({ describe }, node: NodeApis) => { nodePath: node.path, nodeOptions: [], }); - expect(stdout).toBe('Fails as expected 1\nfoo bar\nfoo bar\nFails as expected 2'); + expect(stdout).toMatch(/Fails as expected 1\nfoo bar file\.ts\?tsx-namespace=\d+\nfoo bar file\.ts\?with-query=&tsx-namespace=\d+\nFails as expected 2/); }); test('mts from commonjs', async () => {