diff --git a/src/backend/esbuild/eval.ts b/src/backend/esbuild/eval.ts new file mode 100644 index 000000000..f1bad4114 --- /dev/null +++ b/src/backend/esbuild/eval.ts @@ -0,0 +1,8 @@ +export const createEsbuildEvalTransformSync = (esbuild: typeof import('esbuild')) => (evalCode: string, inputType?: string) => esbuild.transformSync( + evalCode, + { + loader: 'default', + sourcefile: '/eval.ts', + format: inputType === 'module' ? 'esm' : 'cjs', + }, +).code; diff --git a/src/utils/transform/get-esbuild-options.ts b/src/backend/esbuild/get-esbuild-options.ts similarity index 100% rename from src/utils/transform/get-esbuild-options.ts rename to src/backend/esbuild/get-esbuild-options.ts diff --git a/src/backend/esbuild/index.ts b/src/backend/esbuild/index.ts new file mode 100644 index 000000000..dd2791fdf --- /dev/null +++ b/src/backend/esbuild/index.ts @@ -0,0 +1,12 @@ +import type { TransformOptions } from 'esbuild'; +import type { Backend } from '..'; +import { createEsbuildEvalTransformSync } from './eval'; +import { createEsbuildReplTransform } from './repl'; +import { createEsbuildTransformSync, createEsbuildTransform } from './transform'; + +const getEsbuildBackend = (esbuild: typeof import('esbuild')): Backend => ({ + evalTransformSync: createEsbuildEvalTransformSync(esbuild), + replTransform: createEsbuildReplTransform(esbuild), + transformSync: createEsbuildTransformSync(esbuild), + transform: createEsbuildTransform(esbuild), +}); export default getEsbuildBackend; diff --git a/src/backend/esbuild/repl.ts b/src/backend/esbuild/repl.ts new file mode 100644 index 000000000..2cc8f175e --- /dev/null +++ b/src/backend/esbuild/repl.ts @@ -0,0 +1,15 @@ +export const createEsbuildReplTransform = (esbuild: typeof import('esbuild')) => async (code: string, filename: string): Promise => esbuild.transform( + code, + { + sourcefile: filename, + loader: 'ts', + tsconfigRaw: { + compilerOptions: { + preserveValueImports: true, + }, + }, + define: { + require: 'global.require', + }, + }, +).then(result => result.code); diff --git a/src/utils/transform/index.ts b/src/backend/esbuild/transform.ts similarity index 83% rename from src/utils/transform/index.ts rename to src/backend/esbuild/transform.ts index 8448df331..cad6bf1ea 100644 --- a/src/utils/transform/index.ts +++ b/src/backend/esbuild/transform.ts @@ -1,22 +1,19 @@ import { pathToFileURL } from 'node:url'; import { - transform as esbuildTransform, - transformSync as esbuildTransformSync, - version as esbuildVersion, type TransformOptions, type TransformFailure, } from 'esbuild'; -import { sha1 } from '../sha1.js'; +import { sha1 } from '../../utils/sha1.js'; import { version as transformDynamicImportVersion, transformDynamicImport, -} from './transform-dynamic-import.js'; -import cache from './cache.js'; +} from '../../utils/transform/transform-dynamic-import.js'; +import cache from '../../utils/transform/cache.js'; import { applyTransformersSync, applyTransformers, type Transformed, -} from './apply-transformers.js'; +} from '../../utils/transform/apply-transformers.js'; import { cacheConfig, patchOptions, @@ -33,8 +30,8 @@ const formatEsbuildError = ( throw error; }; -// Used by cjs-loader -export const transformSync = ( +// used by cjs-loader +export const createEsbuildTransformSync = (esbuild: typeof import('esbuild')) => ( code: string, filePath: string, extendOptions?: TransformOptions, @@ -63,7 +60,7 @@ export const transformSync = ( const hash = sha1([ code, JSON.stringify(esbuildOptions), - esbuildVersion, + esbuild.version, transformDynamicImportVersion, ].join('-')); let transformed = cache.get(hash); @@ -77,7 +74,7 @@ export const transformSync = ( const patchResult = patchOptions(esbuildOptions); let result; try { - result = esbuildTransformSync(_code, esbuildOptions); + result = esbuild.transformSync(_code, esbuildOptions); } catch (error) { throw formatEsbuildError(error as TransformFailure); } @@ -94,7 +91,7 @@ export const transformSync = ( }; // Used by esm-loader -export const transform = async ( +export const createEsbuildTransform = (esbuild: typeof import('esbuild')) => async ( code: string, filePath: string, extendOptions?: TransformOptions, @@ -109,7 +106,7 @@ export const transform = async ( const hash = sha1([ code, JSON.stringify(esbuildOptions), - esbuildVersion, + esbuild.version, transformDynamicImportVersion, ].join('-')); let transformed = cache.get(hash); @@ -123,7 +120,7 @@ export const transform = async ( const patchResult = patchOptions(esbuildOptions); let result; try { - result = await esbuildTransform(_code, esbuildOptions); + result = await esbuild.transform(_code, esbuildOptions); } catch (error) { throw formatEsbuildError(error as TransformFailure); } diff --git a/src/backend/index.ts b/src/backend/index.ts new file mode 100644 index 000000000..a64d4cf77 --- /dev/null +++ b/src/backend/index.ts @@ -0,0 +1,23 @@ +import esbuild from 'esbuild'; +import type { Transformed } from '../utils/transform/apply-transformers'; + +import getEsbuildBackend from './esbuild'; + +export interface Backend { + evalTransformSync: (evalCode: string, inputType?: string) => string; + replTransform: (code: string, filename: string) => Promise; + transformSync: ( + code: string, + filePath: string, + extendOptions?: ExtendedTransformOptions, + ) => Transformed; + transform: ( + code: string, + filePath: string, + extendOptions?: ExtendedTransformOptions, + ) => Promise; +} + +const backend = getEsbuildBackend(esbuild); + +export default backend; diff --git a/src/cjs/api/module-extensions.ts b/src/cjs/api/module-extensions.ts index 08b3618e6..8ca65692f 100644 --- a/src/cjs/api/module-extensions.ts +++ b/src/cjs/api/module-extensions.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import Module from 'node:module'; import type { TransformOptions } from 'esbuild'; -import { transformSync } from '../../utils/transform/index.js'; +import backend from '../../backend/index.js'; import { transformDynamicImport } from '../../utils/transform/transform-dynamic-import.js'; import { isESM } from '../../utils/es-module-lexer.js'; import { shouldApplySourceMap, inlineSourceMap } from '../../source-map.js'; @@ -140,7 +140,7 @@ export const createExtensions = ( // CommonJS file but uses ESM import/export || isESM(code) ) { - const transformed = transformSync( + const transformed = backend.transformSync( code, filePath, { diff --git a/src/cli.ts b/src/cli.ts index 24b8f4fb5..2e6075176 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,9 +2,6 @@ import { constants as osConstants } from 'node:os'; import type { ChildProcess, Serializable } from 'node:child_process'; import type { Server } from 'node:net'; import { cli } from 'cleye'; -import { - transformSync as esbuildTransformSync, -} from 'esbuild'; import { version } from '../package.json'; import { run } from './run.js'; import { watchCommand } from './watch/index.js'; @@ -14,6 +11,7 @@ import { } from './remove-argv-flags.js'; import { isFeatureSupported, testRunnerGlob } from './utils/node-features.js'; import { createIpcServer } from './utils/ipc/server.js'; +import backend from './backend'; // const debug = (...messages: any[]) => { // if (process.env.DEBUG) { @@ -206,16 +204,12 @@ cli({ if (evalType) { const { inputType } = interceptedFlags; const evalCode = interceptedFlags[evalType]!; - const transformed = esbuildTransformSync( + const transformed = backend.evalTransformSync( evalCode, - { - loader: 'default', - sourcefile: '/eval.ts', - format: inputType === 'module' ? 'esm' : 'cjs', - }, + inputType, ); - argvsToRun.unshift(`--${evalType}`, transformed.code); + argvsToRun.unshift(`--${evalType}`, transformed); } // Default --test glob to find TypeScript files diff --git a/src/esm/hook/load.ts b/src/esm/hook/load.ts index e4965623d..7e7868a05 100644 --- a/src/esm/hook/load.ts +++ b/src/esm/hook/load.ts @@ -1,9 +1,9 @@ import { fileURLToPath } from 'node:url'; import path from 'node:path'; -import type { LoadHook } from 'node:module'; +import type { LoadHook, LoadHookContext } from 'node:module'; import { readFile } from 'node:fs/promises'; import type { TransformOptions } from 'esbuild'; -import { transform, transformSync } from '../../utils/transform/index.js'; +import backend from '../../backend/index.js'; import { transformDynamicImport } from '../../utils/transform/transform-dynamic-import.js'; import { inlineSourceMap } from '../../source-map.js'; import { isFeatureSupported, importAttributes, esmLoadReadFile } from '../../utils/node-features.js'; @@ -56,11 +56,10 @@ export const load: LoadHook = async ( } if (isJsonPattern.test(url)) { - if (!context[contextAttributesProperty]) { - context[contextAttributesProperty] = {}; - } - - context[contextAttributesProperty]!.type = 'json'; + // @types/node only declares `importAttributes` type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context[contextAttributesProperty as keyof LoadHookContext] ||= {} as any; + (context[contextAttributesProperty as keyof LoadHookContext] as ImportAttributes).type = 'json'; } const loaded = await nextLoad(url, context); @@ -91,7 +90,7 @@ export const load: LoadHook = async ( * which are already in CJS syntax. * In CTS, module.exports can be written in any pattern. */ - const transformed = transformSync( + const transformed = backend.transformSync( code, filePath, { @@ -118,7 +117,7 @@ export const load: LoadHook = async ( loaded.format === 'json' || tsExtensionsPattern.test(url) ) { - const transformed = await transform( + const transformed = await backend.transform( code, filePath, { diff --git a/src/patch-repl.ts b/src/patch-repl.ts index da4ea6179..865d0cd2e 100644 --- a/src/patch-repl.ts +++ b/src/patch-repl.ts @@ -1,27 +1,11 @@ import repl, { type REPLServer, type REPLEval } from 'node:repl'; -import { transform } from 'esbuild'; +import backend from './backend'; const patchEval = (nodeRepl: REPLServer) => { const { eval: defaultEval } = nodeRepl; const preEval: REPLEval = async function (code, context, filename, callback) { try { - const transformed = await transform( - code, - { - sourcefile: filename, - loader: 'ts', - tsconfigRaw: { - compilerOptions: { - preserveValueImports: true, - }, - }, - define: { - require: 'global.require', - }, - }, - ); - - code = transformed.code; + code = await backend.replTransform(code, filename); } catch {} return defaultEval.call(this, code, context, filename, callback); diff --git a/src/repl.ts b/src/repl.ts index d44fbb74d..48c3bc58e 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -2,7 +2,7 @@ import repl, { type REPLEval } from 'node:repl'; import { version } from '../package.json'; -import { transform } from './utils/transform/index.js'; +import backend from './backend/index.js'; // Copied from // https://github.com/nodejs/node/blob/v18.2.0/lib/internal/main/repl.js#L37 @@ -16,7 +16,7 @@ const nodeRepl = repl.start(); const { eval: defaultEval } = nodeRepl; const preEval: REPLEval = async function (code, context, filename, callback) { - const transformed = await transform( + const transformed = await backend.transform( code, filename, { diff --git a/tests/index.ts b/tests/index.ts index 6661521a8..7ca952f50 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -5,7 +5,7 @@ import { nodeVersions } from './utils/node-versions'; (async () => { await describe('tsx', async ({ runTestSuite, describe }) => { await runTestSuite(import('./specs/repl')); - await runTestSuite(import('./specs/transform')); + await runTestSuite(import('./specs/transform-esbuild')); for (const nodeVersion of nodeVersions) { const node = await createNode(nodeVersion); diff --git a/tests/specs/transform.ts b/tests/specs/transform-esbuild.ts similarity index 89% rename from tests/specs/transform.ts rename to tests/specs/transform-esbuild.ts index 2d943e66e..7b17c68c5 100644 --- a/tests/specs/transform.ts +++ b/tests/specs/transform-esbuild.ts @@ -2,7 +2,10 @@ import { testSuite, expect } from 'manten'; import { createFsRequire } from 'fs-require'; import { Volume } from 'memfs'; import outdent from 'outdent'; -import { transform, transformSync } from '../../src/utils/transform/index.js'; +import esbuild from 'esbuild'; +import getEsbuildBackend from '../../src/backend/esbuild/index.js'; + +const backend = getEsbuildBackend(esbuild); const base64Module = (code: string) => `data:text/javascript;base64,${Buffer.from(code).toString('base64')}`; @@ -41,7 +44,7 @@ export default testSuite(({ describe }) => { describe('transform', ({ describe }) => { describe('sync', ({ test }) => { test('transforms ESM to CJS', () => { - const transformed = transformSync( + const transformed = backend.transformSync( fixtures.esm, 'file.js', { format: 'cjs' }, @@ -64,7 +67,7 @@ export default testSuite(({ describe }) => { }); test('dynamic import', () => { - const dynamicImport = transformSync( + const dynamicImport = backend.transformSync( 'import((0, _url.pathToFileURL)(path).href)', 'file.js', { format: 'cjs' }, @@ -75,7 +78,7 @@ export default testSuite(({ describe }) => { test('sourcemap file', () => { const fileName = 'file.mts'; - const transformed = transformSync( + const transformed = backend.transformSync( fixtures.ts, fileName, { format: 'esm' }, @@ -93,7 +96,7 @@ export default testSuite(({ describe }) => { test('quotes in file path', () => { const fileName = '\'"name.mts'; - const transformed = transformSync( + const transformed = backend.transformSync( fixtures.ts, fileName, { format: 'esm' }, @@ -112,7 +115,7 @@ export default testSuite(({ describe }) => { describe('async', ({ test }) => { test('transforms TS to ESM', async () => { - const transformed = await transform( + const transformed = await backend.transform( fixtures.ts, 'file.ts', { format: 'esm' }, @@ -133,7 +136,7 @@ export default testSuite(({ describe }) => { test('sourcemap file', async () => { const fileName = 'file.cts'; - const transformed = await transform( + const transformed = await backend.transform( fixtures.ts, fileName, { format: 'esm' },