Skip to content

TS Build api failed when using node ESM module (mjs) #56366

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
stevebeauge opened this issue Nov 10, 2023 · 12 comments Β· Fixed by #57133
Closed

TS Build api failed when using node ESM module (mjs) #56366

stevebeauge opened this issue Nov 10, 2023 · 12 comments Β· Fixed by #57133
Assignees
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status.
Milestone

Comments

@stevebeauge
Copy link

πŸ”Ž Search Terms

ESM ts.sys undefined

πŸ•— Version & Regression Information

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

⏯ Playground Link

https://github.com/stevebeauge/tsc-api-esm

πŸ’» Code

import * as tsc from "typescript";

const tsSystem = tsc.sys;

console.log({ tsSystem });

πŸ™ Actual behavior

If I transpile this code into CJS and run node index.cjs works as expected (tsc.sys is defined).

If I transpile this code into ESM and run node index.mjs the code does not works as expected (tsc.sys is undefined).

πŸ™‚ Expected behavior

tsc.sys (System) should be available in CLI tools, whether the output is ESM or CJS.

Additional information about the issue

The issue occurs when trying to create a CLI tool that require parsing tsconfig files, including potential "extends". Typescript provides an API for that.

However, my cli tool requires to be ESM because of some imports not available as CJS.

Repro steps (using pnpm but it does not matter):

git clone https://github.com/stevebeauge/tsc-api-esm
cd tsc-api-esm
pnpm i 
node index.cjs
node index.mjs

When using cjs : OK
When using ESM : KO

My investigations leds me to :

&& typeof module === "object";

It populates the tsc.sys when the function "isNodeLikeSystem" returns true. However, the test expect to have module be defined (which is true in CJS, not in ESM).

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Nov 10, 2023
@jakebailey
Copy link
Member

jakebailey commented Nov 10, 2023

The isNodeLikeSystem thing is a red herring; our code is CJS and so node will load it as such. The module type of the importing code cannot affect how other modules are loaded.

The actual problem here is the use of a native namespace import to import TypeScript (without the ES module interop helpers, as you have written .mjs directly). If you write import ts from "typescript";, your code will work as expected.

I'm not sure why node isn't straight up rejecting import * as ts from "typescript". It used to error and force the use of a default import as our module is not one that can be statically analyzed. I hit this myself in dtsBundler.mjs in our own repo. But, I'm not having any luck getting that error to appear again even on old versions of Node and TypeScript.

@stevebeauge
Copy link
Author

I'm not sure to understand the difference between import * as tsc from "typescript"; and import tsc from "typescript";

I confirm it work using this other syntax, so is in my cli tool. I close the issue, as it's probably not a bug.

Many thanks for highlighting the wrong import.

@RyanCavanaugh RyanCavanaugh added Question An issue which isn't directly actionable in code and removed Needs Investigation This issue needs a team member to investigate its status. labels Nov 10, 2023
@jakebailey

This comment was marked as outdated.

@jakebailey
Copy link
Member

A small example:

// mod.cjs
const exported = { value: 1234 }
console.log("hello from mod.cjs! exported =", exported);
module.exports = exported;

// index.mjs
import * as asNamespace from "./mod.cjs";
import asDefault from "./mod.cjs";

console.log({ asNamespace, asDefault });

Prints:

hello from mod.cjs! exported = { value: 1234 }
{
  asNamespace: [Module: null prototype] { default: { value: 1234 } },
  asDefault: { value: 1234 }
}

So, what I thought was weird was in fact not (and I should have logged the whole object and not ust one prop). When you do the namespace import, you get an object with only default, so asNamespace.value is undefined (like ts.sys). See also: https://nodejs.org/api/esm.html#commonjs-namespaces

But, I can't actually seem to get TS to emit some sort of static error to complain about this:

image

Nor can I figure out how to get Node to emit some error here.

@jakebailey
Copy link
Member

Going to reopen this just because I want to still investigate some way we can help people avoid this footgun.

@jakebailey jakebailey reopened this Nov 10, 2023
@jakebailey jakebailey added Needs Investigation This issue needs a team member to investigate its status. and removed Question An issue which isn't directly actionable in code labels Nov 10, 2023
@jakebailey jakebailey self-assigned this Nov 10, 2023
@jakebailey jakebailey added this to the Backlog milestone Nov 10, 2023
@stevebeauge
Copy link
Author

Actually, the mjs and cjs files are not manually written in my case. This results from compiling a ts file (src/index.tx in my repro repo). Maybe typescript itself should detect the case ?

Thanks for your detailed explanations anyways.

@jakebailey
Copy link
Member

Do you have esModuleInterop enabled? You probably should.

@stevebeauge
Copy link
Author

Do you have esModuleInterop enabled? You probably should.

Yes I do.

@nodech

This comment was marked as off-topic.

@jakebailey

This comment was marked as off-topic.

@jakebailey
Copy link
Member

I looked at this harder and realized that the error I previously saw was when you try and do something like:

import { isSourceFile } from "typescript";

This will give an error like:

SyntaxError: Named export 'isSourceFile' not found. The requested module 'typescript' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'typescript';

But, import * as ts does not actually ask for a specific member, so doesn't fail outright. cjs-module-lexer reports:

{ exports: [], reexports: [] }

So it's "correctly" detecting that we don't have any static exports, but them being empty doesn't mean "no namespace imports allowed".

Downloading TypeScript 4.9 and running cjs-module-lexer on it produces the same result, and similarly does not complain, so this is nothing new; TypeScript's API has always had this problem.

@jakebailey
Copy link
Member

I realized that we could use the same trick esbuild does and sent #57133, which fixes this issue, allowing named imports and making the namespace import actually work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
5 participants