microsoft/TypeScript

ts.transpileModule outputs commonjs with "module": "nodenext" and package.json "type": "module"

Opened this issue ยท 4 comments

๐Ÿ”Ž Search Terms

nodenext, commonjs, transpileModule, module, esm

๐Ÿ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about nodenext

โฏ Playground Link

https://stackblitz.com/edit/wpaw3gcw?file=build.js

๐Ÿ’ป Code

index.ts

export function test() {
  console.log('test')
}

build.js

import * as ts from 'typescript';
import { basename, dirname, resolve } from 'node:path';
import { readFileSync } from 'node:fs';

const fileName = resolve('./index.ts');
const tsconfigPath = ts.findConfigFile(fileName, ts.sys.fileExists);
const { config, error } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
if (error) {
  throw error;
}

const compilerOptions = ts.parseJsonConfigFileContent(
  config,
  ts.sys,
  dirname(tsconfigPath),
  undefined,
  basename(tsconfigPath)
).options;
const transpileResult = ts.transpileModule(readFileSync(fileName, 'utf8'), {
  compilerOptions,
  fileName,
});
// Should not contain require or exports
console.log(transpileResult);

tsconfig.json

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "declaration": true,
    "target": "ES2022",
    "module": "nodenext"
  }
}

๐Ÿ™ Actual behavior

When transpiling a file with ts.transpileModule it is transpiled to commonjs even though "type": "module" is configured in the related package.json

๐Ÿ™‚ Expected behavior

Transpiling a file with ts.transpileModule should transpile it to esm with "type": "module" configured in the related package.json

Additional information about the issue

From an initial analysis, this seems to be because in the transpile phase, the provided host cannot read the package.json, as the fileExists from the host will return false for an existing package.json.

This is intentional. See: #53022 (comment)

The workaround with assigning a fileName with .mts will falsify the sourcemap output, which can be fixed, but is inconvenient.
The behavior is also very surprising, as just changing the module from esnext to nodenext will suddenly cause a different output format.

The correct "workaround" would be to do the package.json reading yourself, determine the correct output format, then pass that to module instead of nodenext. transpileModule is intentionally not accessing the file system; that's what the API is designed for. You can use the normal Program-based emit flow instead if you want a file system accessing API.

That is fine. I still contend that this behavior is very surprising.
But feel free to close.