Skip to content
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

Proposed tsconfig.json changes #3019

Closed
evanw opened this issue Mar 24, 2023 · 20 comments
Closed

Proposed tsconfig.json changes #3019

evanw opened this issue Mar 24, 2023 · 20 comments

Comments

@evanw
Copy link
Owner

evanw commented Mar 24, 2023

Here are all of the currently-open changes related to tsconfig.json files:

Common themes are: problems when tsconfig.json overrides esbuild's settings, problems when tsconfig.json doesn't override esbuild's settings, and problems when esbuild's interpretation of tsconfig.json affects code in node_modules.

This issue documents my thoughts about how to solve these issues.

Background

The typical workflow with the TypeScript compiler is that each run of tsc converts TypeScript files to JavaScript one-at-a-time. Then you either run the JavaScript directly, or bundle it and then run that. Each execution of tsc has at most one root tsconfig.json file. So if your TypeScript project is split into two directories (each with its own tsconfig.json file), you'd have to run tsc at least twice.

For convenience, esbuild added support for loading some settings from tsconfig.json files. It was originally just baseUrl, then paths, then target, and more. Here's the full list:

However, the way that esbuild does it is different than tsc. Each source file is affected by the tsconfig.json file in the nearest enclosing directory for that source file. This lets you use esbuild to bundle a TypeScript project that is split into two directories (each with its own tsconfig.json file) in a single step instead of having to run esbuild at least twice like you have to do with tsc (which would be slower and potentially generate worse code).

This approach that esbuild took initially made sense for baseUrl and paths, but doesn't really make sense for target, and is iffy for some of the other settings as well. The reason why this doesn't make sense for target is that you probably wouldn't want part of your bundle to be lowered to ES5 while another part of your bundle to use ESNext syntax.

What to do about this

At this point esbuild is widely-used, so any change to this is going to break someone. But this needs to be changed for these issues to be fixed. So this decision is also about deciding which workflows to break.

Arguably supporting multiple tsconfig.json files at once was a mistake because tsc doesn't work like that. So at the moment I'm thinking about removing support for doing this. That would fix a lot of these issues, and would likely also help align how esbuild works with people's existing mental model for how TypeScript works. But I'm sure this will break someone so I'd only do it in a breaking change release, and I'm also talking about doing it in this issue ahead of time.

Another issue is that TypeScript only applies tsconfig.json to TypeScript files by default, while esbuild processes both TypeScript and JavaScript files by default. So for example "target": "ES6" lowers code in both .ts and .js files to JavaScript, "jsx": "preserve" applies to both .tsx and .jsx, and baseUrl and paths are interpreted in both .ts and .js files (including those in node_modules).

Here's one proposal to address these outstanding issues:

  • Change esbuild to only use a single tsconfig.json file

  • Most tsconfig.json settings would no longer apply to file paths containing a node_modules path segment. Also, most tsconfig.json settings would no longer apply to JavaScript files unless you use "allowJs": true or jsconfig.json. Specifically:

    Setting When it's applied
    jsx target Always applied to all files
    importsNotUsedAsValues preserveValueImports useDefineForClassFields Only applied to TypeScript files, except it's not applied when bundling is enabled and the TypeScript file is in node_modules
    alwaysStrict baseUrl jsxFactory jsxFragmentFactory jsxImportSource paths Applied to TypeScript files (and to JavaScript files if allowJs is enabled), except it's not applied when bundling is enabled and the TypeScript file is in node_modules
  • Remove esbuild's support for TypeScript's moduleSuffixes setting (this was requested by someone)

  • Change esbuild to default useDefineForClassFields to true (i.e. use JavaScript semantics for TypeScript class fields by default). This is worth calling out because tsc defaults useDefineForClassFields to false, so this means that esbuild would compile class fields differently than TypeScript by default. The underlying reason is that esbuild's target defaults to esnext while TypeScript's target defaults to ES5, and TypeScript also has some weird behavior where class fields behave differently depending on the target. I currently have esbuild emulating TypeScript's weird behavior when you explicitly set a target, but if you leave everything at their default values, esbuild currently defaults useDefineForClassFields to false like tsc. Changing esbuild to default useDefineForClassFields to true has been requested several times, so I'm planning to do it, but it's very subtle and I'm guessing it will cause lots of unfortunate breakage. Much more info and history about this is here: esbuild's default esnext target doesn't include useDefineForClassFields: true, unlike TypeScript #2993 (comment).

I also need to add support for TypeScript 5.0's new verbatimModuleSyntax setting and extends multiple inheritance, which I could do along with these other tsconfig.json changes.

@evanw
Copy link
Owner Author

evanw commented Mar 24, 2023

I almost forgot: I also have to change esbuild's decorator support to check the experimentalDecorators setting. Right now esbuild unconditionally treats all decorators in TypeScript files as if experimentalDecorators was always enabled. But now that JavaScript is getting decorators too, esbuild should only do this if experimentalDecorators is actually enabled in tsconfig.json.

@jakebailey
Copy link

I have yet to read the whole thing and think about it yet (probably Monday when I'm back in work-mode), but regarding:

Arguably supporting multiple tsconfig.json files at once was a mistake because tsc doesn't work like that. So at the moment I'm thinking about removing support for doing this. That would fix a lot of these issues, and would likely also help align how esbuild works with people's existing mental model for how TypeScript works.

I find this sorta weird because multi-tsconfig is sort of how TypeScript works... if you're using build mode and project references, or even if you have two tsconfigs that would theoretically include different files. I think that's how most large projects are structured.

For example, now that TypeScript itself is bundled by source through esbuild, we do have many tsconfigs that could apply different settings for their respective folders. I don't think I have anything set in those that would actually matter, but, I wouldn't put it past someone to have some packages of a monorepo at one target and one at another, or some part of their codebase that uses one JSX factory style but another somewhere else. But, these are really silly and I expect nobody to do them either.

I'd have to think harder about this, though. Clearly, there's a lot of options listed above, and I've definitely read that useDefineForClassFields thread and been very confused as to how we ended up where we are.

@evanw
Copy link
Owner Author

evanw commented Mar 25, 2023

Thanks very much for weighing in. Sorry for writing so much text about this 😅

I'm not sure what to do because some tsconfig.json settings only really make sense in a global context (e.g. target) while others could make sense in a local context (e.g. jsxFactory). For example, right now if you try to use tsconfig.json to set the compile target using target, it'll only apply to files in that directory. It won't apply for example to esbuild's runtime code or to virtual files created by plugins, since those aren't anchored to the file system. People don't expect it to work that way.

Another problem seems to be that people have random tsconfig.json files lying around and are surprised that for example a stray "jsx": "preserve" causes esbuild to generate invalid JavaScript. Which is what esbuild was told to do, but it's still unexpected. People have said that esbuild's global jsx setting should override tsconfig.json to fix this, but that would prevent you from using tsconfig.json to tell esbuild "build everything with --jsx=automatic (an esbuild setting) by default except for this code over there which needs "jsx": "react" (a tsconfig.json setting) instead".

I could create some inconsistent case-by-case rules for which individual fields come from which tsconfig.json file. But that doesn't seem like a good idea. Ideally there would be a consistent rule that's easy to communicate. Doing tsconfig.json files by parent directory is one consistent rule. Only having one tsconfig.json file is another consistent rule.

Here's another proposal that would be a smaller change:

  • Keep the per-directory tsconfig.json files

  • Add the not-applying-to-JavaScript except for allowJs: true

  • Add the not-applying-in-node_modules

  • Only respect tsconfig.json fields that make sense in a local context

    • This means removing support for target in tsconfig.json files. People won't expect that because it seems like it should make sense in a context where there is only one tsconfig.json file. But perhaps that's ok. It's easy to set the target with esbuild, and also esbuild's target value is different than TypeScript's anyway since it supports specific JS engines.

      I'm not sure what to do about useDefineForClassFields in that case though. Maybe just always default to true (the opposite of TypeScript) unless you explicitly set useDefineForClassFields: false in your tsconfig.json file? It's legacy behavior anyway so maybe that's ok.

    • Maybe I should just ignore "jsx": "preserve" and "jsx": "react-native" in tsconfig.json but respect all of the other settings? Since emitting JSX vs. emitting JavaScript is sort of a global decision, while using the old-style JSX transform vs. the new-style JSX transform is probably more of a local decision. Then you'd have to do --jsx=preserve at the esbuild level if you wanted that.

    • This approach seems like it should work with a few special cases now. But it might cause trouble in the future if TypeScript introduces more global-like settings in tsconfig.json that are relevant to esbuild. Going along with this direction, that would mean esbuild would have to not support them. Maybe that's fine.

@tido64
Copy link

tido64 commented Mar 25, 2023

Hi @evanw, thanks for this write-up. moduleSuffixes has been a pain point for us in particular. It was introduced as a generic solution to type checking React Native platform forked files. In React Native, one would use platform suffixes for platform specific code. As an example, a React Native project could have the following files:

// bar.android.ts (Android specific file)
export default (x: string) => string;

// bar.ios.ts (iOS specific file)
export default null;

// bar.ts (fallback, typically used to target web)
export default (x: string) => string;

// foo.ts
import bar from "./bar";
bar("x");

Without moduleSuffixes, tsc would resolve ./bar to bar.ts and ignore the other files. It wouldn't catch that bar.ios.ts didn't export a function.

With moduleSuffixes, we can now tell tsc to "target" certain platforms. To target iOS, we would pass [".ios", ".native", ""] to tsc. Likewise, to target Android, we would pass [".android", ".native", ""]. The official React Native bundler, Metro, fully supports platform forked files. It resolves modules as described when a target platform is specified.

esbuild evaluating this field per package is where we get into trouble. esbuild can pick up files for other platforms than the user intended. A dependency could have "moduleSuffixes": [".android", ".native", ""] set in tsconfig.js because Android just happened to be the last thing they checked before publishing it, causing issues for users that want to target iOS. Dependencies should not be able to do this.

IMO, moduleSuffixes is useful when you need to type check all forks, but makes less sense when you're trying to bundle. It would make sense to me if esbuild only evaulated moduleSuffixes once, if it was specified at the project root. This would be most similar to how Metro does it. I would also be fine with it being removed since the plugin API should allow this anyway.

Update: I've just learned that --resolve-extensions already solves this. Ignoring moduleSuffixes altogether sounds like the best alternative.

cc @afoxman @sverrejoh

@sandersn
Copy link

Re useDefineForClassFields: it is indeed very subtle, but it's unlikely to cause widespread breakage for a couple of reasons:

  1. The main one is that it's an error to write code that relies on the difference in semantics between the two -- specifically, an accessor overriding a property or vice versa. That's been the case since the flag was available, or maybe a couple of versions afterward. So all the breaks have already happened.

  2. Before I added that error, I checked a lot of open source code and asked for people at Google and Bloomberg to survey their codebases as well. We found very few cases where [[Set]] (Typescript) vs [[Define]] (ES) mattered and even fewer that used [[Set]]-exclusive semantics both intentionally and correctly. There were definitely breaks, but they were all concentrated on a few people who were intentionally and correctly exploiting [[Set]] semantics--and who were rightly annoyed at having to move to ES-standard [[Define]].

@jakebailey
Copy link

jakebailey commented Mar 29, 2023

I brought this up in a TypeScript design meeting to try and get extra eyes on this. I think there are a few on the team who are going to be looking at this harder so we can talk about it again next week with some more context and opinions (as clearly this is nuanced enough to require some thought).

I assume that you're not going to implement these changes "soon", right, given the break-y-ness?

(I'm probably going to hold off on giving my thoughts until I've put them together as I've been busy with other stuff!)

@evanw
Copy link
Owner Author

evanw commented Apr 1, 2023

Thanks for your replies. I can implement this either soon or not soon, depending on people's input. From this it sounds like I should wait for people to think about it more. I'd like to make the change reasonably soon though because it's responsible for a cluster of tsconfig-related issues that have been broken for a long time.

@jakebailey
Copy link

My raw notes are at https://gist.github.com/jakebailey/1e4df8b3a33d2da7b0db4ae39861afd8, but here's the general feelings I have with more context.

This is going to be a large brain dump, so I'm totally happy to break this down in follow ups. Also, these are generally just "my opinions", other team members may also provide their own views, but what I have written up below didn't seem obviously wrong to anyone who read it, so I'm sending it over!


Generally, I think that esbuild's behavior should be guided by "what would happen if a user ran tsc and then ran esbuild on the output?". I feel like that (with some exceptions), leads to the most "expected" behavior.

Unfortunately (for the you and the implementation), I think there are too many gotchas for not supporting multiple tsconfigs. I know that there's ambiguity surrounding which tsconfig is the right one to use for a particular file, but, I think the downsides outweigh the benefits, for the most part. There are a lot of options that I think people very well might set per-project and expect it to work, e.g. paths, useDefineForClassFields and experimentalDecorators, and maybe even the JSX settings.

So, as a long list, my personal recommendations for the listed options are roughly:

  • Entirely ignore tsconfig in node_modules. Better yet, ignore .ts in node_modules too. If esbuild weren't involved, the .js files will be executed, not the .ts files, so I would expect a bundler to not resolve TS source in this case.
    • Gotcha: monorepos with symlinks? But, how do those projects actually resolve anything either, especially if they are using typescript's outDir?
  • Ignore target, let it be controlled by esbuild globally. It doesn't make sense to have this configured per project.
    • Gilding the lily: warn if target is unset, but tsconfig has a value.
    • This is the exception to the "what if tsc then esbuild" idea.
  • Ignore alwaysStrict and always assume strict. For TS code, it's going to become effectively impossible to write code without being in strict mode thanks to us deprecating some options, plus the inability to gauge where any particular bundle may execute. (The TS playground allows you to set all of the right options to disable "use strict", but then is unable to leave strict mode itself to run that code!)
  • Ignore moduleSuffixes and recommend esbuild's resolveExtensions, which is how it works in webpack and matches the user expectation of "global config".
    • They can set [".ios.ts", ".native.ts", ".ts", ...].
  • Allow multiple jsx settings per-project. It's possible that there are monorepos with mixed environments.
    • I would not be unhappy if this weren't actually done; doing it would be consistent with "run tsc and then esbuild", as all that info will have been locked in at compile time. However, it feels exceedingly unlikely that this can happen.
  • Respect useDefineForClassFields+target for TS code in choosing class emit style.
    • Alternatively, enabling this by default may also be fine; all "bad" code for this should only ever be in the user's project, not their dependencies. They can discover the problem themselves, right?
  • Respect experimentalDecorators for TS code for figuring out which decorator to use.
    • Unlike JSX or useDefineForClassFields, this one is going to be prolific and not simple; while class define semantics are similar enough to probably not cause a problem, "standard" decorators are completely different to "legacy" ones, and "legacy" decorators are not going to go away any time soon.
  • Respect paths/baseUrl, in the nearest tsconfig. There is really no way to determine which tsconfig is the one to emit any particular file, so this seems like the only reasonable thing, besides giving esbuild a list of tsconfig files that could apply and then have it run the globs to determine which one is the most applicable.
    • I just don't see people stopping using these options for more than just describing runtime resolution. Many popular frameworks suggest using paths like @/* for src (e.g. vue: https://github.com/vuejs/create-vue/blob/main/template/tsconfig/base/tsconfig.json), and if I have multiple projects like this, there is no single tsconfig I could provide that makes it work.
    • Gilding the lily: warn if that tsconfig's include/exclude/files do not include that file.
  • Respect importsNotUsedAsValues, preserveValueImports, verbatimModuleSyntax? This really only depends on how much side effect behavior esbuild is wanting to emulate. Unfortunately us adding a third variant means more work, because dropping the other two means not supporting TS <5.0.
    • I'm sure you were already going to do this; I'm just including it last for completeness!

This is a bit of a condensed brain dump, so I'm totally happy to spitball and discuss the above.

Alternatively, there is another option... which is to completely ignore tsconfig and instead increase the configurability of esbuild to capture some of these cases, and/or move some behavior into plugins. For example:

  • There is prior art for webpack and paths in the form of tsconfig-paths-webpack-plugin.
  • jsx options could be path-scoped.
  • useDefineForClassFields and experimentalDecorators could be path scoped too.

But, I don't know if that's going to be the most user-friendly option either.

@evanw
Copy link
Owner Author

evanw commented Apr 10, 2023

Thank you very much again for such thoughtful and detailed replies. I really appreciate it.


My replies

Generally, I think that esbuild's behavior should be guided by "what would happen if a user ran tsc and then ran esbuild on the output?".

I'm assuming you mean "if a user ran tsc once for each project (i.e. directory tree with a tsconfig.json file), then ran esbuild once to bundle everything together" and not "if a user processed all .ts files with a single tsc command, then ran esbuild on the output". Since you're talking about keeping support for multiple tsconfig.json files, you'd need to run tsc multiple times. Correct me if this is wrong.

Entirely ignore tsconfig in node_modules. Better yet, ignore .ts in node_modules too.

I suspect there are some people that are doing this intentionally (via Hyrum's Law). For example, I could see it being useful to ensure that const enum is inlined, since bundling from the original source code results in better code in that case. It would certainly be simpler for esbuild to refuse to process .ts files in node_modules, but I suspect doing that will break some people. I swear I remember hearing about some people doing this but I can't find it right now.

I'm wondering if a good change here could be to shift all TypeScript file extensions to last in the "resolve extensions" order when inside node_modules. That way if you explicitly import a .ts file or if the .js file doesn't exist at all, then the package clearly exists for the purpose of providing TypeScript source code (and esbuild should probably also respect any tsconfig.json file also in that package). But people that accidentally publish their .ts alongside their .js will no longer have the .ts picked up (and thus any published tsconfig.json will also be ignored, since we're making tsconfig.json no longer apply to .js files).

Regarding monorepos with symlinks: Like node, esbuild defaults to using the real path of an input file as its identity. So if there are symlinks in node_modules that point outside of node_modules, the real path of any of those input files won't include node_modules. You can change that behavior with esbuild's --preserve-symlinks setting. So I guess in that case esbuild's behavior would differ depending on the value of that setting, but by default esbuild would prefer .ts files over .js files in that case.

Respect useDefineForClassFields + target for TS code in choosing class emit style.

Specifically: esbuild will check the value of target in tsconfig.json only for the purpose of determining a default value for useDefineForClassFields, and not for affecting esbuild's own target setting. The value will be determined using logic that goes something like this:

if ('useDefineForClassFields' in tsconfig)
  esbuild.useDefineForClassFields = tsconfig.useDefineForClassFields;
else if ('target' in tsconfig)
  esbuild.useDefineForClassFields = greaterThanOrEqual(tsconfig.target, 'ES2022');
else
  esbuild.useDefineForClassFields = true; // esbuild will now be defaulting to true here instead of false

Respect importsNotUsedAsValues, preserveValueImports, verbatimModuleSyntax?

Yes, I plan to add support for verbatimModuleSyntax but not drop importsNotUsedAsValues and preserveValueImports. I'm planning to determine esbuild's behavior using logic that goes something like this:

if (tsconfig.verbatimModuleSyntax === true)
  esbuild.unusedImportFlags |= UnusedImportFlags.KeepPath | UnusedImportFlags.KeepValues;
if (tsconfig.preserveValueImports === true)
  esbuild.unusedImportFlags |= UnusedImportFlags.KeepValues;
if (tsconfig.importsNotUsedAsValues === 'preserve' || tsconfig.importsNotUsedAsValues === 'error')
  esbuild.unusedImportFlags |= UnusedImportFlags.KeepPath;

why would anyone want "preserve" as a target in esbuild?

For the same reason as TypeScript I imagine. People may be doing a custom JSX transform. For example, some people apparently remove certain React properties for tests. SolidJS also apparently has its own JSX transform that compiles to template strings with DOM manipulation.


Updated proposal

  • Keep the per-directory tsconfig.json files
  • All tsconfig.json settings no longer affect any .js files
  • Prefer matching .js files over .ts files in node_modules directory trees (so all implicit .ts extensions would always be tried last)
  • The search for an enclosing directory with a tsconfig.json file stops at a node_modules directory
  • All code in .ts files is now always in strict mode
  • Files passed via esbuild's stdin feature will use the tsconfig settings in the working directory tree
  • Specific tsconfig.json settings with behavior changes:
    • alwaysStrict will now be ignored
    • experimentalDecorators will now be respected (within that directory subtree)
    • jsx will now be ignored if set to preserve or react-native, otherwise it will be respected within that directory subtree (note that react-jsx will be treated as react-jsxdev if esbuild's --jsx-dev is set, which is already what happens today)
    • moduleSuffixes will now be ignored
    • strict will now be ignored
    • target will now be ignored, except for determining the default value of useDefineForClassFields (I agree warning about an ignored target seems like a good idea)
    • verbatimModuleSyntax will now be respected (within that directory subtree)
    • useDefineForClassFields will now default to true when unspecified instead of false (note that setting target counts as specifying useDefineForClassFields)
  • Specific tsconfig.json settings that keep the same behavior:
    • baseUrl
    • extends
    • importsNotUsedAsValues
    • jsxFactory
    • jsxFragmentFactory
    • jsxImportSource
    • paths
    • preserveValueImports

Note that this list does not contain a rule saying "ignore all tsconfig.json files in node_modules". If you import "pkg/file.ts" explicitly, then I think esbuild should probably bundle it instead of refusing to bundle it. And in that case esbuild should probably use node_modules/pkg/tsconfig.json for that file if it exists. But if the package doesn't contain a tsconfig.json file, then esbuild probably shouldn't use a tsconfig.json from outside the package (which is why I wrote that the search terminates at a node_modules directory above).

I agree that esbuild needs to be more flexible with setting these values not using tsconfig.json. But that seems like a separate issue (which I should also solve, likely via the plugin API). I think pulling support for tsconfig.json entirely would at this point be very inconvenient for many people without much gain otherwise, and so probably isn't something I should do.

I should probably also mention that esbuild has a --tsconfig= flag that lets you force all .ts files to use a specific tsconfig.json file. I'm planning to have that apply to all .ts files in this updated proposal as well.

@jakebailey
Copy link

These tweaks all seem good to me. Some commentary:

Since you're talking about keeping support for multiple tsconfig.json files, you'd need to run tsc multiple times. Correct me if this is wrong.

No, if you have more than one tsconfig, I think it's very possible (and probably more common) to use a single invocation by using project references and tsc -b. E.g. in the TS compiler itself, tsc -b ./src is enough to build all code in the repo, even though there are quite a few tsconfigs.

I think some monorepo solutions don't use project references, but then instead rely on custom orchestration of multiple tsc builds (I think nx?), yes, but that is roughly just "what if we made tsc -b parallel?", but with different performance characteristics and upsides/downsides.

If you import "pkg/file.ts" explicitly, then I think esbuild should probably bundle it instead of refusing to bundle it. And in that case esbuild should probably use node_modules/pkg/tsconfig.json for that file if it exists. But if the package doesn't contain a tsconfig.json file, then esbuild probably shouldn't use a tsconfig.json from outside the package (which is why I wrote that the search terminates at a node_modules directory above).

This to me feels like it's going to end up sometimes producing surprising behavior. Here are some example "spooky" cases that I had thought up:

  • If a project is a part of a monorepo, it's plausible that they have a shared tsconfig.json base config to set some standards across all of their packages. If so, then the tsconfig.json for any given package would contain something like "extends": "../../tsconfig.base.json" such that /packages/my-package/tsconfig.json can refer to /tsconfig.base.json. But, install my-package in someone's node_modules and you end up with /node_modules/my-package/tsconfig.json, which then points to /tsconfig.base.json in the dependent project. What happens here? Is this going to end up working?
  • Similarly, it's possible for a package to depend on say @tsconfig/node12; if the tsconfig.json is shipped, but @tsconfig/node12 is a devDep, what happens then?
  • For both of the above, assuming we disallow these references, how are the settings for useDefineForClassFields or similar determined? We can't resolve all settings anymore, so do they get the TS defaults, which are like ES5 and so therefore get [[Set]] via target's implication?
  • If no tsconfig.json is shipped at all, is the code in node_modules going to function with the local tsconfig?
  • If the upstream package provides TS sources for debuggability, e.g. for source mapping and whatnot, but themselves use a bundler, is that TS code expected to work? That code could itself be using "fake" paths which normally would be bad under tsc, but would be valid when bundled. (My last team was guilty of this! Thankfully, no exported API, only exectuables 😄)

For the latter case, it at least will probably cause esbuild to error out, but the former cases feel like they will cause subtle behavioral differences that may be hard to pin down.

The saving grace is that these sorts of things are exceedinly rare. Most people use something like outDir and so the ts source is placed away from the actual outputs, making it unlikely that even when ts is first in an extension list, that it's never acutally resolved.

Also, this is generally not a problem unique esbuild; back before I worked on TS, the project I worked on had a dep which shipped ts source, but since ts came first in our extension list for webpack, we would pull in that dep's source (and upstream considered it a "feature", even though it broke our loading and required a workaround).

So all in all, I'm not strongly attached to maintaining your current behavior for node_modules and TS; I mainly wanted to find ways to simplify things.

@jakebailey
Copy link

For example, I could see it being useful to ensure that const enum is inlined, since bundling from the original source code results in better code in that case.

Normally, I'd say this is very scary! But, I suppose since this is bundled, it's probably fine? Assuming that external const enums don't make it into the bundle if that package is marked as external?

TS itself uses const enums internally, but then there's a build step that drops const from our d.ts files' enum declarations, ensuring that downstream users don't depend on any particular value at runtime. Otherwise, they may import the const enum, get it inlined into their code, and then we change the enum later and suddenly their code is broken.

@humandad
Copy link

humandad commented May 8, 2023

Just another data point for you. We are currently stuck on version 0.14.50 because we want the automatic jsx transform for local development, but not when we're using esbuild to create bundles. In the bundles we create, react is an external dependency and expected to be a global object. I've tested upgrading esbuild to the current version and I can make it work by dynamically creating a file with the contents of {} then using its path in the Tsconfig property. When the bundling process is done, we then delete the file.

@privatenumber
Copy link
Contributor

Not directly related to tsconfig.json itself, but rather its application (I'm happy to open a separate issue if needed):

In both esbuild-loader and tsx, I use a file matcher to test if a given file falls within the scope of the provided tsconfig.json file (considering files, includes, and excludes). Since this should be employed wherever esbuild is utilized for transforming TypeScript, I was wondering if it would be beneficial to integrate it directly into esbuild.

However, this would introduce a breaking change (hence why I thought discussing it here would be relevant). Additionally, it would require providing the path of the tsconfig.json file as well as making the source file path mandatory for applying the tsconfig (unless, perhaps, if neither are passed in).

evanw added a commit that referenced this issue Jun 9, 2023
@evanw
Copy link
Owner Author

evanw commented Jun 9, 2023

Ok, here are the tsconfig.json-related changes that I have landed for this issue (more details can be found in CHANGELOG.md):

  • Using experimental decorators now requires "experimentalDecorators": true (#104)
  • TypeScript's target no longer affects esbuild's target (#2628)
  • TypeScript's jsx setting no longer causes esbuild to preserve JSX syntax (#2634)
  • useDefineForClassFields behavior has changed (#2584, #2993)
  • Add support for verbatimModuleSyntax from TypeScript 5.0
  • Add multiple inheritance for tsconfig.json from TypeScript 5.0
  • Remove support for moduleSuffixes (#2395)
  • Apply --tsconfig= overrides to stdin and virtual files (#385, #2543)
  • Support --tsconfig-raw= in build API calls (#943, #2440)
  • Ignore all tsconfig.json files in node_modules (#276, #2386)
  • Ignore tsconfig.json when resolving paths within node_modules (#2481)
  • Prefer .js over .ts within node_modules (#3019)

This fixes most of the open issues related to tsconfig.json and unblocks implementing JavaScript decorators. So I'm closing this issue because this this feels like a good set of changes to bundle together into a breaking change release. Thanks again for all of your input in the thread above.

This doesn't include the change to omit tsconfig.json configuration for .js files unless allowJs is enabled (or unless you use jsconfig.json). I wasn't sure about that one since I expect people just use that for type checking and not for transpiling. In any case, that can still be changed in the future if it's clear that it's a problem.

@evanw evanw closed this as completed Jun 9, 2023
@lukeed
Copy link
Contributor

lukeed commented Jun 10, 2023

Sorry it’s a bit late, arrived via release notes (congrats!)

Would it be possible to make .js > .ts configurable? Perhaps by respecting the order of resolveExtensions?

Asking because there’s still a fair amount of packages that do some wonky transpilation, esp re decorators. Including ts sibling files allows a consumer to transpile all TS uniformly using their esbuild installation & config. Additionally, .js can be lowered to an unwanted target, JSX rewritten to , etc

@jakebailey
Copy link

Asking because there’s still a fair amount of packages that do some wonky transpilation, esp re decorators. Including ts sibling files allows a consumer to transpile all TS uniformly using their esbuild installation & config. Additionally, .js can be lowered to an unwanted target, JSX rewritten to , etc

Do you have any examples for this? The decorator case is more or less the exact case why I wanted to ensure that .js was resolved before .ts in node_modules; depending on whether or not tsconfig is shipped or is even valid (it could extend a file not in the package), re-transpiling the code may do the complete wrong thing. Especially once esbuild supports both kinds of decorators.

@lukeed
Copy link
Contributor

lukeed commented Jun 10, 2023

Really just all of transpiled history. The number of implementations that have come and gone over the years to deal with decorators…

The point is do with unwanted output settings. Whether the author is delivering decorators (or anything) as ES3/5/lower-than-I-want, I’m now stuck with it. If they offer the raw TS source, I can transpile using whatever settings I want.

not looking to change the default — just make an escape hatch

@prisis
Copy link

prisis commented Jun 21, 2023

Hey :), can this warning "tsconfig.json" does not affect esbuild's own target setting [tsconfig.json] be somehow disabled?

the tsconfig.json target is used in eslint for a example, so its not possible to remove i from the config...

@evanw
Copy link
Owner Author

evanw commented Jun 21, 2023

Warnings can be changed with a log override: https://esbuild.github.io/api/#log-override. Each warning has the category name in square brackets at the end, which in this case is tsconfig.json.

@evanw
Copy link
Owner Author

evanw commented Jun 29, 2023

For the record (no action needed): Here's an example of someone wanting to use .ts files published in an npm package: #3201. Just keeping track of these cases by linking them here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants