Skip to content

Feature Request: allow change file extension of generated files from .ts #49462

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

Open
5 tasks
bluelovers opened this issue Jun 9, 2022 · 50 comments
Open
5 tasks
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@bluelovers
Copy link
Contributor

Suggestion

🔍 Search Terms

List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily and help provide feedback.

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

add something like

{
  "compilerOptions": {
    "module": "nodenext",
    "targetExtension": ".cjs",
}

📃 Motivating Example

  • when targetExtension is .cjs , all .ts will emit as .cjs, but .mts still is emit as .mjs
  • when targetExtension is .mjs , all .ts will emit as .mjs, but .cts still is emit as .cjs

💻 Use Cases

@IllusionMH
Copy link
Contributor

What are exact use cases and what problem it should resolve?
From first look TS would also need to rewrite extension in generated imports that in no go at this moment #49083 (if I haven't missed anything).

@Josh-Cena
Copy link
Contributor

Major use-case is building dual-package with tsc without any postbuild script, I think.

@milesj
Copy link

milesj commented Jun 10, 2022

You shouldn't be dual building packages anyways (use a wrapper), so I prefer the .cts/.mts constraint that TS indirectly enforces.

@bluelovers
Copy link
Contributor Author

i don't wanna make files of .cts and .mts they are same context
so i wanna one .ts can be .cjs and .mjs

@azu
Copy link

azu commented Jun 11, 2022

I've met same issue when I building dual package.

e.g. https://github.com/azu/check-ends-with-period/tree/v2.0.0 (It is invalid example as dual package)
TypeScript source code is insrc/*.ts and package.json has "type": "module" field.
Also, this repository has two tsconfig files.

I've defined exports field as follows, but this package was treated as ESM because "type": "module" is defined.
As a result, This package can not be requred from CJS without dynamic import.
(Node.js treats *.js file as ESM by "type": "module")

  "exports": {
    ".": {
      "require": "./lib/check-ends-with-period.js",
      "import": "./module/check-ends-with-period.js"
    }
  },

I could not found just works solution without using bundler/post scripts.


If targetExtension option exists, I can resolve this issue by tsconfig file.

  • tsconfig.json + "targetExtension": ".mjs",: generates esm to module/*.mjs from src/*.ts
  • tsconfig.cjs.json + "targetExtension": ".cjs",: generates cjs to lib/*.cjs from src/*.ts

However, this option will need to rewrite import path of source code. It oppsite TypeScript's design goal.

...

Edit(2023-01-14): I've created tsconfig-to-dual-package for avoiding this issue.
This tool add package.json which is { "type": "module" } or { "type": "commonjs" } based on tsconfig's module and outDir option.
In other words, publish *.js as CJS and ESM in a single package.
This mechanism based on following:

@milesj
Copy link

milesj commented Jun 12, 2022

Might I suggest packemon: https://packemon.dev/

Also, why exactly are you dual building? You run the risk of the dual package hazard: https://nodejs.org/api/packages.html#dual-package-hazard It's better to use an ES module wrapper.

@bluelovers
Copy link
Contributor Author

i think this only work on nodejs
does browser support it?

// ./node_modules/pkg/wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name;

@milesj
Copy link

milesj commented Jun 13, 2022

Browsers don't support .cjs/.mjs natively, unless it gets bundled through webpack or a similar tool to .js, and at that point, why even use .cjs/.mjs for browsers?

@Josh-Cena
Copy link
Contributor

Josh-Cena commented Jun 13, 2022

The idea is to use native modules when targeting browsers without any transpilation/bundling, and optionally CommonJS when targeting Node.

@milesj
Copy link

milesj commented Jun 13, 2022

Yes of course, but not if you're using .mjs. At least in @azu's example, their ESM code should be shipped to the browser with .js, and CJS code to Node.js with .cjs (or even just .js too).

We also just need more information, as we're making many assumptions here. The original post doesn't contain much.

@Josh-Cena
Copy link
Contributor

Josh-Cena commented Jun 14, 2022

Wouldn't we be emitting .cjs + .js (+ type: module) when building dual-package purely for Node anyway? It's usually unnecessary to have explicit extensions for both sets of module types. Also, browsers can handle .mjs as well, as long as the MIME type is JavaScript. But to author dual-package of any kind, whether targeting Node or browser, we need at least one subset of the output to have an extension different from the other, and that would require TS to be configurable about this. But I agree we lack some context here.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Jun 20, 2022
@alvis
Copy link

alvis commented Jun 21, 2022

I'm pro @bluelovers's suggestion. The use case for me is publishing dual ESM/CommonJS packages for nodejs.

There are many reasons why we need to publish dual ESM/CommonJS packages. For instance, not until 4.7, TS doesn't even allow a node application in commonjs to consume a pure ESM library due to the lack of support on await import(...). So I really find it annoying some package authors publish pure ESM packages.

@cefn
Copy link

cefn commented Dec 4, 2022

@milesj the use of an ESM wrapper around CommonJS defeats tree-shaking, doesn't it? Given ESM has broken the whole ecosystem to try and achieve results like tree-shaking, having the recommended way to align with ESM being to throw away its core features is disappointing. Correct me if there is a reasonable way to get both. My expectation is to follow the principle that modules should be stateless - a good practice I don't ever find the need to violate. Then I understand there are no concerns with dual building.

@milesj
Copy link

milesj commented Dec 4, 2022

If you have a dual package, and some other package in CJS context requires your package, and another package in ESM context imports your package, you'll end up with 2 copies of your package. For node this doesn't matter too much unless there's some kind of global/shared state, but for bundlers this is bad.

@gfortaine
Copy link

I've met same issue when I building dual package.

e.g. https://github.com/azu/check-ends-with-period/tree/v2.0.0 (It is invalid example as dual package) TypeScript source code is insrc/*.ts and package.json has "type": "module" field. Also, this repository has two tsconfig files.

I've defined exports field as follows, but this package was treated as ESM because "type": "module" is defined. As a result, This package can not be requred from CJS without dynamic import. (Node.js treats *.js file as ESM by "type": "module")

  "exports": {
    ".": {
      "require": "./lib/check-ends-with-period.js",
      "import": "./module/check-ends-with-period.js"
    }
  },

I could not found just works solution without using bundler/post scripts.

If targetExtension option exists, I can resolve this issue by tsconfig file.

  • tsconfig.json + "targetExtension": ".mjs",: generates esm to module/*.mjs from src/*.ts
  • tsconfig.cjs.json + "targetExtension": ".cjs",: generates cjs to lib/*.cjs from src/*.ts

However, this option will need to rewrite import path of source code. It oppsite TypeScript's design goal.

@azu @milesj It looks like that tsc-multi might be worth exploring cc @tommy351 #18442 (comment)

@azu
Copy link

azu commented Jan 14, 2023

I've created tsconfig-to-dual-package for avoiding my issue that is described in #49462 (comment)
This tool add package.json which is { "type": "module" } or { "type": "commonjs" } based on tsconfig's module and outDir options.
It resolve my issue by adding each type pacakge.json to lib/(CJS) and module/(ESM) instead of use .cjs and .mjs
This behavior is described in following:

In other words, Both(CJS and ESM) are *.js.
The result is much closer to what I wanted to do.

$ tsc -p . && tsc -p ./tsconfig.cjs.json && tsconfig-to-dual-package
#                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#                                          \ I want to remove it!

This approch pros is that no require addtional build/transpiler tool(no modify output code of tsc).
Cons is that need to copy package.json to outDir.
(Of course, dual package hazard is in here, but this hazard is also in browser's iframe/realm or multiple versions of the same library. Not ideal, I believe the actual harm is limited.)

📝 Note

TypeScript may change the moduleResolution, but did not likely change the output.

I feel like there is confusion everywhere about ESM support. Therefore, I thought that this is not an issue of TypeScript configuration, but rather an ecosystem-wide issue that needs to move forward.

@gfortaine
Copy link

I've created tsconfig-to-dual-package for avoiding my issue that is described in #49462 (comment) This tool add package.json which is { "type": "module" } or { "type": "commonjs" } based on tsconfig's module and outDir options. It resolve my issue by adding each type pacakge.json to lib/(CJS) and module/(ESM) instead of use .cjs and .mjs This behavior is described in following:

In other words, Both(CJS and ESM) are *.js. The result is much closer to what I wanted to do.

$ tsc -p . && tsc -p ./tsconfig.cjs.json && tsconfig-to-dual-package
#                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#                                          \ I want to remove it!

This approch pros is that no require addtional build/transpiler tool(no modify output code of tsc). Cons is that need to copy package.json to outDir. (Of course, dual package hazard is in here, but this hazard is also in browser's iframe/realm or multiple versions of the same library. Not ideal, I believe the actual harm is limited.)

📝 Note

TypeScript may change the moduleResolution, but did not likely change the output.

I feel like there is confusion everywhere about ESM support. Therefore, I thought that this is not an issue of TypeScript configuration, but rather an ecosystem-wide issue that needs to move forward.

@mobsense @jwalton @owenallenaz

@fabis94
Copy link

fabis94 commented May 10, 2023

I don't get the "you shouldn't be dual building" opinion, clearly there's a lot of fragmentation in the ecosystem currently and while everyone's moving in the direction of using ESM, there's still a lot of CJS projects and that's not going to change overnight. Because of this reason if you're building a library, it's important to output both in CJS and MJS.

Additionally, "type": "module" and "moduleResolution": "nodenext"/"node16" are the future, and in a project with these set any CJS build results outputted with a .js extension simply won't work.

Thus, it makes perfect sense to me that instead of outputting results with a vague ".js" extension which causes issues and headaches, you'd want to have explicit .cjs and .mjs extensions for CommonJS and ESModule modules respectively. On top of that, outputted declaration files should have d.cjs and .d.mjs extensions respectively, cause another issue I've ran into multiple times recently is library authors outputting results in .mjs and .cjs, but leaving the declaration files with the .d.ts extension which TypeScript then won't be able to pick up on.

@leeren
Copy link

leeren commented Jul 12, 2023

What are people currently using to work around this? Separate build steps with manual file extension rewriting?

@azu
Copy link

azu commented Jul 12, 2023

I believe that Dual Package can be achieved broadly as follows.

  1. Write two source codes CJS and ESM respectively
    • Write two source codes by hand
    • Handwritten, which is expensive to maintain.
  2. Generate the CJS code from one source code and make the ESM a wrapper that only imports CJS.
  3. Generate the ESM code from one source code and make the CJS a wrapper that only imports ESM.
    • The majority of the source code is in ESM format.
    • The reverse pattern of 2.
    • The CJS entry point imports the ESM via Dynamic Import.
    • Limitations: synchronous APIs cannot be provided from the CJS format.
    • Dynamic Import Proxy pattern.
    • e.g. Prettier v3, Vite
  4. Generate CJS and ESM code from one source code

@ruojianll
Copy link

ruojianll commented Aug 14, 2024

I need this nowadays, but we may not need it in the future, so keep it silence. Let time forget it.

@Tofandel
Copy link

Tofandel commented Nov 12, 2024

If we have "module": "commonjs" then the extension is always .cjs;
If we have "module": "es6", or es2015, es2020, es2022, esnext then the extension is always .mjs;

I would tweak this a bit so that you get .cjs in the first case if your package.json contains "type": "module" and .js otherwise
And vice versa for the second case, you get .mjs if package.json doesn't contain "type": "module" and .js otherwise

@ceztko
Copy link

ceztko commented Nov 12, 2024

I would tweak this a bit so that you get .cjs in the first case if your package.json contains "type": "module" and .js otherwise
And vice versa for the second case, you get .mjs if package.json doesn't contain "type": "module" and .js otherwise

I think "type": "module" is evaluated only with "module": "node16" or nodenext automatic module detection. In my suggestion, the extension is driven first by the module setting in tsconfig.json, and automatically enforces a qualified extension which in some cases is really desired/required.

In my understanding your tweak, as you call it, would introduce a slack rule unconditionally evaluating "type": "module" unregarding of module setting in tsconfig.json, and would not supply a stable extension for people that need an early enforcement of the module convention.

Anyway, other than few thumbs up, I don't see any reaction that suggests the general experience will be improved any time soon.

@jedwards1211
Copy link

jedwards1211 commented Dec 11, 2024

@RyanCavanaugh because many people use the default "moduleResolution" setting, which ignores export maps, the only way we can support subpath imports like import * from 'foo/bar' in a dual package without any TS users running into problems is to have bar.js and bar.mjs files in the root of the package. I'm pretty sure this is unavoidable for many major packages.

I'm also pretty sure @azu's tsconfig-to-dual-package won't work for people with default "moduleResolution" because it builds the CJS and ESM into subdirectories: azu/tsconfig-to-dual-package#15

I haven't seen any TS dual-package authoring tools that seem unproblematic. The popular tsc-alias uses regexes to rewrite import statements, and I'm guessing breaks source maps too.

If tsc would just add quality ways to build dual packages, it would be a lot easier for everyone to avoid the subtle pitfalls of popular tools and approaches.

@RyanCavanaugh
Copy link
Member

I haven't seen any TS dual-package authoring tools that seem unproblematic

The whole reason we don't want to do dual-emit is that there doesn't seem to be an unproblematic way to do it. What can TS do that they can't?

@morganney
Copy link

If you set moduleResolution to nodenext it’s possible to generate dual builds with tsc using only one tsconfig.json and package.json file. Anybody authoring a library today that wants to support dual build should be using nodenext anyways.

@jedwards1211
Copy link

jedwards1211 commented Dec 12, 2024

@RyanCavanaugh what makes general-purpose tools for dual-emit problematic is they have to either copy the source code to a temporary directory and codemod the import path extensions before compiling, running the risk of something breaking in the temporary location, or they have to destructively modify the source code in place before compiling.

Only tsc or babel (or maybe a 3rd-party tool that wraps the tsc internal API?) can codemod import paths in memory, transpile, and then output to a file with the desired extension.

I use a custom babel plugin for it, but of course most TS devs will not prefer this.
There's tsc-multi, which does seem to wrap TS internal API, but if you look at the source code it's very involved compared to hypothetically doing this in tsc.

And it's hard to make a majority of the community aware of 3rd-party tools for this the way tsc could make everyone aware of a new option.

Modifying the import path extensions is really simple compared to most of the transpilation that tsc does, changing the output file extensions is even simpler, and these options would be useful for a variety of dual package layouts.

And if tsc provided the option, most package authors would discover it and be able to get their dual package builds right from the get go instead of many getting it wrong.

@morganney are you talking about having both .(c)ts and .(m)ts in your source code? I've dealt with this problem for years and the only dual package layout I've found that supports the widest variety of situations (1. subpath imports from the package 2. package users who aren't using nodenext 3. not simply re-exporting the CJS modules from ES wrappers) is to transpile .ts source to both CJS and ESM files side by side, provide an export map for people whose tools can consume it, and fall back to relative path resolution to either the CJS or ESM files for users whose tools ignore the export map.

@morganney
Copy link

morganney commented Dec 12, 2024

@jedwards1211

No, I’m talking about a build tool that runs tsc twice while updating the type in package.json and rewriting specifiers. Use whatever file extension you want.

Which is more or less what you’re saying. The biggest hurdle to this being baked into tsc is that they refuse to rewrite specifiers.

Hopefully with the ability to require(esm) the need for dual building will grow less and less important.

@jedwards1211
Copy link

jedwards1211 commented Dec 12, 2024

Right, there are edge cases (e.g. subpath imports) you can only support by rewriting specifiers, and doing that inside tsc would be way more flexible and convenient than doing it with external tools.

Hopefully with the ability to require(esm) the need for dual building will grow less and less important.

If I did my research, this still requires an command line flag in Node 22, which doesn't reach end of life until April 2027, so if we want to support all users of LTS Node, we still have to dual build for another 2 1/2 years.

The choice between being conservative or helping the JS ecosystem run more smoothly for those 2 1/2 years is in the TS developers' hands.

@heath-freenome
Copy link

heath-freenome commented Apr 3, 2025

I just encountered a situtation where I want to make the output of a commonjs compile be .cjs extension rather than .js. I am forced to rename the file after running the tsc compiler. Why is there so much resistance to just telling the compiler to output a different extension from .js, especially when specifying the commonjs module type?

@RyanCavanaugh
Copy link
Member

Why is there so much resistance to just telling the compiler to output a different extension from .js?

You are already able to do this by naming your input files .cts

@jedwards1211
Copy link

jedwards1211 commented Apr 3, 2025

@RyanCavanaugh I'm sure you can understand that we would already be using .cts if that satisfied our needs here.

To put it another way, why is there so much resistance to building in easier options for transpiling the same source to both CJS and ESM? Do y'all view that as a fundamentally bad idea?

@ceztko
Copy link

ceztko commented Apr 3, 2025

You are already able to do this by naming your input files .cts

@RyanCavanaugh: sometimes there's a resistance of the developer/teams to do so. I suggest again an alternative solution which I think it would be cleaner than a brutal "targetExtension": ".cjs" or "targetExtension" : ".mjs". I was thinking something like a "ensureQualifiedExtension" : "true" (or similarly named option) that behaves like the following:

  • If we have "module": "commonjs" then the extension is always .cjs;
  • If we have "module": "es6", or "es2015", "es2020", "es2022", "esnext" then the extension is always .mjs;
  • If we have "module": "node16" or "nodenext" then the extension respects the rules for module detection as documented and the final extension can be either .cjs or .mjs.

@RyanCavanaugh
Copy link
Member

Do y'all view that as a fundamentally bad idea?

Not as much "bad" as "impossible". It's in general not possible to take a valid ESM program and turn it into a valid CJS program, or vice versa, because dependencies can and do present arbitrarily different shapes depending on what resolution mode you're working in.

In the cases where it is possible, it's only through a sort of syntax-only processing that any third-party tool can do; TS cannot add useful information to this process. We don't consider ourselves to be in the business of implementing or re-implementing every possible JS-related build transform that can exist (examples: TS does not minify, obfuscate, code-split, tree-shake, or bundle outputs), so we recommend using one of those tools if you're in a situation where a mechanical CJS<->ESM transform would work close enough for your scenario.

I was thinking something like a "ensureQualifiedExtension" : "true" (or similarly named option) that behaves like the following

Why do you need .cjs output file extensions? Setting "type": "commonsjs" in package.json should generally have the same results.

See also https://www.typescriptlang.org/docs/handbook/modules/appendices/esm-cjs-interop.html#conclusions

@ceztko
Copy link

ceztko commented Apr 3, 2025

Why do you need .cjs output file extensions? Setting "type": "commonsjs" in package.json should generally have the same results

Oh yes, I knew it already. In a project I also ended pushing a package.json with with a single line in the output folder:

{
  "type": "commonjs"
}

At the time I considered this solution as an acceptable trade-off for the inability of the compiler to unconditionally set a qualified extension. I still think that if the compiler could be told to emit a qualified extension with the rules I suggested above it would be a more elegant solution.

@jedwards1211
Copy link

jedwards1211 commented Apr 3, 2025

Why do you need .cjs output file extensions? Setting "type": "commonsjs" in package.json should generally have the same results.

So, many packages output CJS and ESM to separate directories and use a package.json in each to control module type.

This works for importing the root of the package, but breaks down when you want to support subpath imports from your package like import { bar } from 'foo/bar'. You would think you could support this with export map rules like "./*": { "types": { "import": "./dist/esm/*.d.ts", "default": "./dist/cjs/*.d.ts" }, "import": "./dist/esm/*.js", "default": "./dist/cjs/*.js" }, but if a user has the default TypeScript moduleResolution setting, TypeScript will ignore the export map and fail to find 'foo/bar'. In that case it can only find it if ./bar.js exists in the package root directory. I don't think any package authors want to tell users they have to use a specific moduleResolution setting, so the only option is to build either the CJS or the ESM to the package root directory.

I mean I guess you could avoid changing extensions if you build CJS into the root directory and then ESM into a dist/esm subdirectory, since TS is only going to resolve the ESM in a mode that consults export maps anyway. But I don't like this as a general-purpose solution because there would be problems if the source itself has a dist/esm directory (unlikely, but I don't want any potential gotchas with a build system I'm trying to employ across a large number of packages). So I prefer to output .mjs files parallel to the .js files (or vice versa, .js/.cjs files with "type": "module" in the package.json).

If there are any simpler ways to support all module types and resolution modes with subpath imports, I'm all ears.

@jedwards1211
Copy link

jedwards1211 commented Apr 3, 2025

It's in general not possible to take a valid ESM program and turn it into a valid CJS program, or vice versa, because dependencies can and do present arbitrarily different shapes depending on what resolution mode you're working in.

I guess you wouldn't want to provide this feature unless it were possible for it to work perfectly, but I wouldn't expect it to figure out what shape the imported module would have (since that depends on the runtime environment anyway). There are cases where I have to call an interop helper in my source code to make the transpiled ESM work in Node. I would have to call that interop helper anyway if my package were pure ESM.

I just wish TS would merely transform import sources to resolved paths, since it would be cleaner and more foolproof than what build tools do. Since TS-transpiled ESM importing from CJS is subject to the limitations of how Node and other tools analyze the exports of the CJS file, it would be most reasonable to expect the same limitations if TS provided an option to transform output extensions and import paths.

@RyanCavanaugh
Copy link
Member

I just wish TS would merely transform import sources to resolved paths, since it would be cleaner and more foolproof than what build tools do

I don't understand this statement. One of two things is true:

  • The mapping between import source and resolved path is trivial, in which case all solutions are equally foolproof
  • The mapping between import source and resolved path is nontrivial, in which case it can't possibly be foolproof at all - just try to make sense of the output of --traceResolution or consider the length of documentation needed to explain module resolution at a high level. Adding an extra dimension to that matrix (what is the transform of an arbitrary path in the presence of arbitrary config) can't possibly make things easier to understand

@morganney
Copy link

@jedwards1211 all module resolutions except classic support package.json imports and exports fields. Maybe node10 is an exception too, but nobody should be using those module resolutions anyway.

TS will never rewrite extensions or imports so finding a third-party tool is your best bet.

@jedwards1211
Copy link

jedwards1211 commented Apr 4, 2025

@morganney isn't classic still the default from tsc --init/omitting the field or has that changed? It doesn't matter what people should use if the default is still widespread. If they shouldn't use it TS should change the default

@jedwards1211
Copy link

jedwards1211 commented Apr 4, 2025

@RyanCavanaugh sorry it wasn't clear, no module resolution isn't trivial. But it's not what generally causes problems for dual builds -- usually CJS module shapes when imported into ESM causes the problems, as you noted.

What I mean is not "foolproof" about third party dual build tools is the fact that they either have to temporarily overwrite the source files before calling tsc, or copy the project and source files to temp directories, or run an instrumented compiler via undocumented TS APIs that are not semver, or use a third party compiler (which is less of an authority on TS syntax and semantics). In each of these four options, it feels like various things could go wrong aside from the CJS or ESM transform.

@RyanCavanaugh
Copy link
Member

use a third party compiler (which is less of an authority on TS syntax and semantics)

Right, and what you're proposing - that TS have some notion of how to rewrite import specifiers - makes this problem worse, not better. There's a rich and excellent ecosystem of other tools that can transform TS to JS, many of which can do their jobs very correctly without needing to import/replicate TS, and making the runtime behavior of an output TS program dependent on TS's resolution semantics would make this tools' jobs much much harder.

@jedwards1211
Copy link

jedwards1211 commented Apr 4, 2025

@RyanCavanaugh I'm talking about the fact that if TS adds a new syntax in the future, third party tools will lag behind in their support of it. Or if TS changes its internal API, third party tools that use it would suddenly break. Can you understand how that feels unnerving? And it would feel more secure to only have to depend on tsc? Just because there are a lot of good tools out there doesn't make the situation close to ideal

@morganney
Copy link

morganney commented Apr 4, 2025

Accept that ESM and CJS are not generally compatible. For instance no live bindings or TLA in CJS. Then do this:

  1. Run your compilation with tsc against a module system, say esm.
  2. Dynamically alter type in package.json and module/moduleResolution in TS config.json to the other module system, say cjs.
  3. Run another compilation with tsc against the dynamic config changes.
  4. Rewrite extensions and specifiers for the output of the dual build.
  5. Restore original configs.

Have to build twice but now you’re in sync with the current state of tsc api.

But, be aware of some issues with how tsc handles differences between module systems. There are cases where the compiler won't error, but the runtime does: #58658

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests