`.cts` files with `import`/`export` fail differently depending on how they are loaded

Closed
#811 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

A pull request for this has already been merged.

  • #815 by @Brooooooklyn — merged

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
node.js, typescript
Domain
backend, devtools

Research direction

Start by reading the .cts loading paths and load_commonjs_esm, then inspect the CommonJS transform behavior described in the issue; no file paths or tests are named. Compare entry-point, ESM import, and require() loading against the provided reproduction. Done means these paths handle .cts files with import/export consistently, either by compiling to CommonJS or by applying one consistent alternative.

Written by the indexing model from the issue text.

Description

A .cts file that uses import/export fails in three different ways depending on how it is loaded, and loads fine in one case. TypeScript (module: nodenext) and tsx compile such a file to CommonJS, which is common in TypeScript projects, for example for config files.

Reproduction
// package.json
{ "type": "module" }
// both.cts
class Counter {
  count = 1;
}
export function describe(): number {
  return new Counter().count;
}
console.log('both.cts evaluated, require:', typeof require);
// import.ts
import { describe } from './both.cts';
console.log('import ->', describe());
// require.ts
import { createRequire } from 'node:module';
console.log('require ->', createRequire(import.meta.url)('./both.cts').describe());

Results on Node.js 24.21 (22.18 and 26.5 behave the same):

Command @oxc-node/core 0.1.5 tsx 4.23.15 Node.js type stripping
both.cts as the entry point ERR_REQUIRE_CYCLE_MODULE runs, require: function SyntaxError: Unexpected token 'export'
import.ts does not provide an export named 'describe' import -> 1 does not provide an export named 'describe'
require.ts ReferenceError: require is not defined in ES module scope (from the helper require()) require -> 1 SyntaxError: Unexpected token 'export'
require('./both.cts') from a .cjs file in a CommonJS package runs, require: undefined runs, require: function SyntaxError: Unexpected token 'export'

In the last row oxc-node runs the file as an ES module, so require, module, and __dirname are undefined inside it.

Why

.cts is CommonJS by contract here: load_commonjs_esm only flips ambiguous extensions to ES modules, and the CommonJS transform emits require() for helpers but leaves import/export in place, since oxc has no ESM-to-CommonJS module transform. Node.js then sees ESM syntax in a CommonJS file and takes a different path depending on the caller.

Possible directions
  1. Match tsc and tsx: compile the module syntax to CommonJS. This needs an ESM-to-CommonJS module transform in oxc.
  2. Until then, either run such a file as an ES module consistently on every path (named exports, entry points, and require() would then work, with no CommonJS globals inside), or reject it with one clear error, as Node.js does.

Vite+ currently takes the first half of option 2 in its vendored copy: a .cts with module syntax runs as an ES module on both the synchronous and asynchronous hook paths, and a .cts without module syntax stays CommonJS.

Dominant language
TypeScript
Stars
275
Forks
10
Avg merge
5h 31m
Merged PRs (30d)
68

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from oxc-project/oxc-node

All issues in oxc-project/oxc-node

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.