From 807f467299c20ead6d8629b055f5cb695284a526 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Sat, 8 Jun 2024 12:02:31 +0900 Subject: [PATCH] fix(esm): cjs interop to support decorators fixes https://github.com/privatenumber/tsx/issues/582 --- src/esm/hook/load.ts | 13 +++++-------- src/utils/transform/index.ts | 4 ++++ tests/specs/api.ts | 20 +++++++++++++++++--- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/esm/hook/load.ts b/src/esm/hook/load.ts index 2c3d43619..81f51df66 100644 --- a/src/esm/hook/load.ts +++ b/src/esm/hook/load.ts @@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url'; import type { LoadHook } from 'node:module'; import { readFile } from 'node:fs/promises'; import type { TransformOptions } from 'esbuild'; -import { transform } from '../../utils/transform/index.js'; +import { transform, transformSync } from '../../utils/transform/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'; @@ -83,17 +83,14 @@ export const load: LoadHook = async ( * In fact, the code can't even run because imports cannot be resolved relative * from the data: URL. * - * TODO: extract exports only + * This should pre-compile for the CJS loader to have a cache hit + * TODO: extract exports only? */ - const transformed = await transform( + const transformed = transformSync( code, filePath, { - format: 'cjs', - - // CJS Annotations for Node - platform: 'node', - // TODO: disable source maps + tsconfigRaw: fileMatcher?.(filePath) as TransformOptions['tsconfigRaw'], }, ); diff --git a/src/utils/transform/index.ts b/src/utils/transform/index.ts index 973bf9e3a..8448df331 100644 --- a/src/utils/transform/index.ts +++ b/src/utils/transform/index.ts @@ -53,6 +53,10 @@ export const transformSync = ( define, banner: '(()=>{', footer: '})()', + + // CJS Annotations for Node. Used by ESM loader for CJS interop + platform: 'node', + ...extendOptions, } as const; diff --git a/tests/specs/api.ts b/tests/specs/api.ts index d3e3e3e2a..7fc29c7e2 100644 --- a/tests/specs/api.ts +++ b/tests/specs/api.ts @@ -23,7 +23,15 @@ const tsFiles = { export const foo = \`foo \${bar}\` as string export const async = setTimeout(10).then(() => require('./async')).catch((error) => error); `, - 'exports-no.cts': 'console.log("cts loaded" as string)', + 'exports-no.cts': ` + // Supports decorators + const log = (target, key, descriptor) => descriptor; + class Example { + @log + greet() {} + } + console.log("cts loaded" as string) + `, 'exports-yes.cts': 'module.exports.cts = require("./esm-syntax.js").default as string', 'esm-syntax.js': 'export default "cts export"', 'bar.ts': 'export type A = 1; export { bar } from "pkg"', @@ -36,6 +44,11 @@ const tsFiles = { }), 'index.js': 'import "node:process"; export const bar = "bar";', }, + 'tsconfig.json': createTsconfig({ + compilerOptions: { + experimentalDecorators: true, + }, + }), ...expectErrors, }; @@ -527,7 +540,8 @@ export default testSuite(({ describe }, node: NodeApis) => { ...tsFiles, }); - const { stdout } = await execaNode(fixture.getPath('import.mjs'), [], { + const { stdout } = await execaNode('./import.mjs', [], { + cwd: fixture.path, nodePath: node.path, nodeOptions: [], }); @@ -658,6 +672,7 @@ export default testSuite(({ describe }, node: NodeApis) => { test('tsconfig disable', async () => { await using fixture = await createFixture({ + ...tsFiles, 'package.json': createPackageJson({ type: 'module' }), 'tsconfig.json': createTsconfig({ extends: 'doesnt-exist' }), 'import.mjs': ` @@ -668,7 +683,6 @@ export default testSuite(({ describe }, node: NodeApis) => { tsconfig: false, }); `, - ...tsFiles, }); await execaNode('import.mjs', [], {