microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.13k stars 12.5k forks source link

bug(esm): TypeScript is not an ECMAScript superset post-ES2015 #50501

Closed ctjlewis closed 5 months ago

ctjlewis commented 2 years ago

Bug Report

This bug report will show that TypeScript is no longer an ECMAScript subset as of ES2015 due to its refusal to support import specifier rewrites for contrived and misplaced reasons.

Preface

This issue is related to previously reported issues, but expands on them and clarifies the scope of the problem.

This team, mostly Andrew and Ryan, have repeatedly shut down these issues in the past. I ask that this issue not be closed on a kneejerk reaction because I intend to speak with others at Microsoft about it.

TS is designed to be an ES superset

TypeScript doubtless has many design goals, and I understand how hard it is to balance all of them. However, undeniably, its core design goal - what TypeScript exists to do - is stated in this repository's description and also throughout the TS docs:

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

We should all agree that TS, first and foremost, is supposed to be an ECMAScript superset. There is no amount of mental gymnastics that gets us out of this. We will proceed with this in mind as we cover the scope of this issue and some of the technical excuses that have been offered to justify not addressing the problem.

What is a superset?

Originally a set theory concept, in terms of language it was best stated by @sepp2k in this StackOverflow answer:

Syntactically a language A is a subset of a language B if every program that is valid in language A is also valid in language B. Semantically it is a subset if it is a syntactic subset and each valid A program also exhibits the same behavior in language B.

To formalize this, Sebastian offers the following definition:

If P_A is the set of all valid programs in language A and P_B is the set of all valid programs in language B, then language A is a syntactic subset of language B exactly if P_A is a subset of P_B.

We will not need to address the semantic subset issue as TS fails the first test for arbitrarily many input programs1, which we will get into.

For convenience, and to emphasize the point that our subset programs are actually emitted JS versions of the input TS program, we will refer to a program written in the superset language as "program T" and the corresponding output in the subset language as "program T'".

Additionally, because TS output depends on your compiler configuration, we will consider TypeScript to pass the semantic test if there is some configuration for which you can transform a valid T into T'.

1 This is something this team is already well aware of, and have simply chosen to ignore. In my judgment, the only possible explanation for violating the core purpose of this technology for such a large portion of its lifecycle is organizational deficiency, and not any practical technical reason.

Is TypeScript a JS superset?

Sometimes. Let's walk through some examples (with approximated and simplified, not literal, output):

  1. ✅ Program T

    const a: string = "hello world!"
    console.log(a);

    ✅ Program T'

    const a = "hello world!"
    console.log(a);

    The input program is valid and the output program is valid. The runtime behavior of T and T' are identical. This example passes both the syntactic and semantic superset tests.

  2. ✅ Program T (using relative extensionless TS import)

    import { a } from "./a"
    console.log(a);

    ❌ Program T' (TS-style imports left untransformed)

    import { a } from "./a"
    console.log(a);

    The input program is valid, but there is no configuration for which you will get a valid ECMAScript module output. The output program T' will throw according to the ES spec. This example fails the syntactic and semantic superset tests. Our perfectly valid TypeScript program cannot be transformed by the compiler into a valid ECMAScript program, and this is easily shown.

    While we can construct a different input program for which we will receive valid output (import { a } from "./a.js"), making that suggestion is proof positive that TS is no longer an ES superset, and there are infinitely many valid input programs which will not emit valid output. Nothing about this is subjective or within the realm of personal judgment.

Bearing this in mind, the last version of ES for which TypeScript was a superset would be ES5 (2009), before the introduction of ES modules.

Why does this matter?

It matters because our industry has struggled to adopt ESM for a variety of reasons, largely related to interop with downstream CJS consumers and also integration with TypeScript. It should be fairly obvious the damage that is done to the ecosystem when the TypeScript compiler very literally cannot generate valid ESM programs without special syntax.

The idea of having to use a special syntax to achieve a correct output program is inherently antithetical to the "TS is an ES superset" principle on which TypeScript is based. It takes a severe amount of technical dysfunction to turn a blind eye to this for years and years, and a serious amount of contempt for users and software quality to continually shut down user feedback on this issue.

Inadequacy of existing solutions and team response to user feedback

I have clarified several times that I am chalking this up to organizational dysfunction and attempting to kick it up the chain myself, as many many many users have raised this issue in isolation only to be shut down and/or directly gaslit by members of this team.

Let's go over some of these issues. I will start with the closest suggestion to a fix currently under consideration, and then go through the rest in chronological order.

(!) 2020: allow voluntary .ts suffix for import paths (#37582) (by @timreichen)

I will start with this suggestion as it comes the closest to fixing the problem, while also offering a two-birds-one-stone benefit with regard to Deno, Bun, etc. support:

let import a from "path/to/a.ts" behave the same as import a from "path/to/a"

As specified, this partially solves our problem. Because import "./a.ts" could only ever refer to a TypeScript program, you cannot conceive of any input .ts import statement which will not correctly build to a .js import and execute equivalently at runtime.

However:

  1. Because it is apparently "core design goal" that the imports never get rewritten, the most important part of this suggestion cannot be honored.

  2. (Very important) This demands you rewrite your existing, valid import "./a" statements to ./a.ts, and your valid ./a imports still do not build to valid ES import statements.

Thus, however we might choose to emit the statements from input, we must either:

  1. Resolve extensionless relative specifiers which are valid TS; OR
  2. Deprecate extensionless relative specifiers in TypeScript, and make them invalid TS, to bring T and T' to parity.

The current approach by this team has literally just been to do nothing and deny that there is a conflict here. This conflict and its relationship to TS as an ES superset is the core focus of this issue.

2017: TypeScript and (#13422) (by @cyrilletuzi)

Because the problem is, again, that TypeScript ESM input programs cannot build to valid output, naturally <script type="module"> goes right out the window.

Same pattern: User opens thread, years of comments and confusion, denialism from team members, summarily closed.

2017: Provide a way to add the '.js' file extension to the end of module specifiers (#16577) (by @quantuminformation)

This issue looks like the others we will see: A user raising the issue that their valid TypeScript program cannot compile to a valid ES program, a large debate between users and various TS maintainers, and eventually the issue being closed, locked, and buried.

As usual, users point out that TS will act like their imports are valid while refusing to resolve them at compile-time, highlighting the tradeoff I explained. Relative extensionless specifiers must either be allowed and transformed, OR disallowed entirely. Doing both only violates the TS superset principle and confuses users because they depend on their output programs being isomorphic with their inputs.

Also, to generalize this issue a bit, I don't think it's actually about adding a .js extension, but resolving the module specifier to an actual path, whatever the extension is.

This comment has 50 likes. The parent issue has nearly 300. Minimizing comments from the team were met with a negative reaction as usual.

2020: Compiled JavaScript import is missing file extension (#40878) (by @jameshfisher)

This is an interaction, and a restatement of the same problem, which is effectively the perfect example of not just the problem and the common sense solution to it, but also the community's investment in this being solved, and the textbook denialism by maintainers. The bot response announcing the closure as "working as intended" has 65 downvotes.

Mr. Fisher raises the issue that a program containing valid TypeScript imports will not emit a valid corresponding ECMAScript program. As we will see, this issue is met with approval and enthusiasm by people who found it trying to understand why their programs will not build (after several hours, they will presumably put two and two together and realize it's just not possible). OP is then promptly sandbagged by members of this team and the issue is erroneously closed.

After the issue is posted, Ryan quickly responds to this post by minimizing OP's complaint and telling him to kick rocks:

TypeScript doesn't modify import paths as part of compilation - you should always write the path you want to appear in the emitted JS, and configure the project as necessary to make those paths resolve the way you want during compilation

Again, as we've covered, this is not how supersets work. Simply telling someone to rewrite every already-valid TS import statement into a different one which will compile should seem absurd on its face given what we've gone over. This comment received about 20 downvotes.

Mr. Fisher responds noting that he was shocked to find referring to TS source files with .js (an extremely confusing and hacky solution that is unbecoming of even a temporary workaround, let alone a permanent solution) actually does resolve his problem. He then lays out a few of the many reasons why this is confusing and hacky. His comment received 50 positive reactions.

Ryan responds with another heavily-downvoted dismissal of this issue:

TS never takes an existing valid JS construct and emits something different, so this is just the default behavior for all JS you could write. Module resolution tries to make all of this work "like you would expect"

You would expect a valid TS import to become a valid JS import because it's supposed to be a superset. It is not a discussion or an issue of perspective; TS claims to be a superset, if it is, the valid imports would remain valid at runtime. Yet they do not, and we have highly experienced and talented engineers effectively lying about what is and is not expected runtime behavior.

@RevealedFrom clarifies this at further length, though it should be pretty obvious already why there are several problems with this, least of all being the superset issue:

@RyanCavanaugh Writing "./dep.js" doesn't sound logical. The file dep.js does not exist in the Typescript universe. This approach requires the coder to know the exact complied output and be fully aware of the compiled environment. It's like having to know the CIL outputted and modify it here and there in order to code in C# successfully. Isn't the whole idea of Typescript to abstract away Javascript?

import { foo } from "./dep" is legitimate Typescript, and it provides the information for Typescript to resolve all that is needed to type check and make the code compile successfully. So, the compiled output should work. Typescript should not be generating syntactically incorrect Javascript.

IMHO, this issue should be a bug.

Ultimately, it is a bug, and we know it is a bug for the reasons we've covered. Ryan responds again with a minimizing and heavily-downvoted statement I would characterize as dishonest at best, and an outright lazy lie at worst:

The whole idea of TypeScript is to add static types on top of JavaScript, not to be a higher-level language that builds to JS.

We have enough background to know this is wrong, though we are starting to get insight into the executive dysfunction that is causing this issue. Core maintainers are making up what the purpose of this technology is as they go along, apparently now privately rejecting that TS is or ever was meant to be a JS superset.

A user in this thread (@Gin-Quin) had to actually implement a bundler to work around this, something I also had to do:

I will add that I indeed created a library called tsc-esm as a workaround for this bug, and even though it works quite fine every time I use it (ie 100% of the times I need to create a library written in Typescript) I feel I should not have to patch the output of the Typescript compiler like I do.

And another user (@djfm) also:

@silassare I think probably a thousand person, me included, have written similar scripts. This is just absurd.

And the last comment from this thread I will cover, and emphasize, is this one by Mr Felber (@PatrickFelber):

This whole discussion is absurd. Valid typescript obviously transpiles to invalid ES6 code. I do not understand in which world this can be classified as "works as intended" behavior. Adding ".js" to import statements which actually refer to ".ts" files as workaround is too wild for me. For my current project I have free choice, therefore I'm gonna switch over to CommonJS target module code. CommonJS works without file extensions. In case one can live with CommonJS target module, this is an option to get around this problem.

The only people who claim to not see the problem are members of this team. Note that this user's only choice was to give up and stay on CJS - this is what I am referring to when I say that this is likely the most major blockade to ESM adoption under Why does this matter?.

2020: TypeScript cannot emit valid ES modules due to file extension issue (#42151) (by me)

This issue was opened by me to clarify that the compiler cannot emit valid ES modules. It received positive feedback from users, but was naturally accompanied by denialism from Ryan and eventually closed and locked. A comment by @richardkazuomiller points out the ridiculousness of the justification offered for not fixing this issue ("we can't rewrite import specifiers"):

Exactly, could someone explain like I'm 5 why it's OK for TypeScript to convert import ... from './file' to const ... = require('./file') changing ./file to ./file.js is not OK?

2021: One year later: TypeScript still cannot emit valid ES modules (#47270) (by me)

This is a rephrasing of the original issue, posted so as to remind this team that this problem has not been resolved. The single response from Ryan was amusing given the context of this issue:

We've explained the rationale behind not modifying import paths on emit many times, and don't see value in rehashing that discussion since it relates to a core design goal. [...] Please file something concrete in the future.

You must decide to prioritize a TypeScript "core design goal", but you can only choose one:

  1. Never rewriting import specifiers, even though the compiler does this for regular import statements
  2. Ensuring TS is an ES superset like it says it is throughout its documentation

Apparently this is not an obvious answer to TS maintainers, though to users it apparently is. Furthermore this issue is an attempt to follow-up explicitly on the second half of Ryan's request to file something concrete going forward, though I imagine this issue will also be summarily buried as this team appears to have only one tool in its toolbox, namely denial for the sake of convenience.

2020: Appending .js extension on relative import statements during Typescript compilation (ES6 modules) (StackOverflow)

Same situation we've seen: User has a valid input program, can't get a valid output program. The answerer prudently notes:

If the compiler simply generated extension-less output files it would also solve the issue. But, would that also somehow violate the design principle regarding URI rewrites? Certainly, in that case there could exist other design principles to defend the position! But wouldn't such stubbornness only help to further validate the adamancy or ignorance of the TS team on this issue?

2022: "module": "node16" should support extension rewriting (#49083) (by @arendjr)

This is the most recent issue that covers this problem. Mr. van Beelen is certainly a lot more generous and forgiving with his wording than I am, though he is equally rewarded with stark denialism from Ryan.

We don't rewrite target-legal JavaScript code to be different JavaScript code from what it started with, no matter the configuration.

We won't even get into the fact that import "./a" is not an ES2015 "target-legal" import statement, as it's just an egregiously incompetent claim. It received about 60 downvotes.

Mr. van Beelen left several more long, thoughtful comments, generating over 100 positive responses, explaining much of the same things that I have covered here, that valid TS programs should become valid JS programs, though he did so with much more forgiving phrasing. I recommend everyone read his comments, and I found them very considerate - likely impractically so.

This is another thread that contains hundreds of comments, hundreds of positive reactions for the sensible position taken by users, and hundreds of negative reactions for this team's excuses. It is impossible to read it all, but it is worth skimming over and noticing that it, like every other issue raised regarding TS's inability to build valid output programs, was summarily closed for the sake of maintainer sanity to the detriment of several million codebases relying on TS.

2022: Add Compileroption to add Extensions to relative imports (#47436) (by @jogibear9988)

A PR to add non-breaking, opt-in compilerOption for adding .js extension to extensionless relative TS imports, which received about 30 positive reactions. Of course, it was summarily closed without a real review. No amount of attempts to contribute by the community have been well-received by this team.

🔎 Search Terms

🕗 Version & Regression Information

TypeScript was initially released in 2012, and it has not been an ES superset since the release of ES2015, which require imports to have file extensions if they are not named modules.

⏯ Playground Link

Cannot repro in playground, requires multiple entry points. Minimum repro: https://replit.com/@ctjlewis/ts-esm-superset

Throws ERR_MODULE_NOT_FOUND because ./a is not a valid ES import specifier.

💻 Code

N/A, see repro and notes in this issue.

🙁 Actual behavior

The valid TS-ESM program T builds to an invalid program T'.

🙂 Expected behavior

The valid TS-ESM program T should build to a valid program T'. Valid TS imports should always compile to valid ES imports for the given target.

mrhyde commented 5 months ago

An easier way would be to use tsfix to handle imports.

guest271314 commented 5 months ago

An easier way would be to use tsfix to handle imports.

At

compiled JavaScript files are valid ESM

import "file"

without a file extension is valid Ecmascript Module syntax.

The simplest solution that as I see it is using/incorporating WICG Import Maps into TypeScript, which in the browsers Chromium 128 and Firefox 128 implements, and Deno JavaScript/TypeScript runtime respectively; with this caveat Deno dynamic import("./exports") throws module not found for "exports.js" dynamically created in the script for Deno re dynamic imports, which dives into precisely what ECMA-262 writes out as to specifiers for Ecmascript Modules, and what it doesn't.

Then you can set your specifiers to be whatever you want - completely user-defined. Controversy and schism over. I don't think it is possible for anybody other than the core TypeScript maintainers to even speculate about whether or not TypeScript is ECMA-262 conformant.

If you are exclusively in Node.js world Node.js doesn't support import maps. Node.js is still using CommonJS as a default module loader.

guest271314 commented 5 months ago

Re

This bug report will show that TypeScript is no longer an ECMAScript subset

I don't even get that far.

From my perspective TypeScript is an entirely different programming language from JavaScript.

For comparison Bun claims to somehow be comparable to Node.js. I start poking around, testing things until they break, and find Bun to be it's own thing; no HTTP/2; no upload streaming, and so forth. bun does have tooling for TypeScript/JavaScript, but it ain't node. node has issues, too. We have to use --experimental-default-type=module just to get to Ecmascript Modules being the default in the context, out of legacy CommonJS support, and so forth. That doesn't change the fact folks are initiating new projects using Ecmascript Modules, thus this issue.

So, I would use Deno if I was into TypeScript to eliminate the node variable of not supporting import maps or Ecmascript Modules by default. It's the same underlying V8 JavaScript/WebAssembly engine.

ctjlewis commented 4 months ago

@RyanCavanaugh - I would probably lock, you guys have made your position on it super clear and this was mostly a troll post to give people a place to vent.

minecrawler commented 4 months ago

this was mostly a troll post to give people a place to vent.

@ctjlewis while it may be a troll post in your opinion, it's about a problem real people have in real life in real projects, working for real corps, earning their very real money and having to spend so much time working around a problem introduced by convention and definition without any technical basis, which is maddening. For all practical purposes and to lessen everyone's burden, so much could be done here. And just in the recent weeks we've seen actual improvements on different sides of this problem.

Yes, not everything is on Microsoft, but TS is a standard in the industry, and having no solution here - at least a documented way of getting things done - is a problem. It's not being addressed, which is a no-go for such a widely used tech, and many workarounds have sprouted in the community which points to the problem being a pain-point.

Hence, closing this post will just lead to the problem popping up somewhere else in another issue, possibly with similar discussions to what we already have here. Something I'd like to see is an actionable plan to resolve this issue. Which doesn't mean that the MS team should just do whatever internet randoms tell them to, but decide on some steps to solve the problem in a way they see fit. Which at least should include a chapter in their tutorials addressing it and showing a recipe for common scenarios, like making a lib for different runtimes and module systems. A best-practice we can just copy and be done with it.

TypeScript is an entirely different programming language from JavaScript.

@guest271314 yes, TS is a different language than JS. They are still both based on ECMA-262 and TS has the explicit goal of supporting simple transpilation to JS and adding to it. See the official landing page for reference: "TypeScript is a strongly typed programming language that builds on JavaScript." At the same time, TS is an industry standard, and should try to play nice with other parts of the ecosystem. No one cares about perfect integration, however simple recipes on how to deal with common scenarios are a baseline.

jogibear9988 commented 4 months ago

for me there are two solutions. Enable the transformer plugins, (like ttypescript does) or add somethink like I did in my pull: https://github.com/microsoft/TypeScript/pull/47436

guest271314 commented 4 months ago

@minecrawler

They are still both based on ECMA-262 and TS has the explicit goal of supporting simple transpilation to JS and adding to it.

ECMA-262 has some omissions. Observable when we dive in to the minutae.

Take Deno as an example. Deno explicitly supports TypeScript. No need for ts-node. If you are in to TypeScript you can run TypeScript out of the box on the command line or in .ts scripts.

Now, let's examine one impact of supporting static TypeScript programming in the dynamic JavaScript programming language that I just happened to encounter while creating a non-Node.js specific version of wbn-sign NPM package. Dynamic import module is not found when created after the application started. #20945 linking to Do not permission prompt for statically analyzable dynamic imports, specifically this comment

I also used to think the same, but it's essentially a given due to how TypeScript supports typings for statically analyzable dynamic imports. Stemming from there it becomes a requirement to preload such imports, which results in things like write("foo.ts"); await import("./foo.ts") being quirky. And it follows that the runtime net access doesn't occur so it shouldn't prompt.

Basically this ship has sailed from the perspective of build tools / static analysis. At this point I think it's viable to carry forward with these behaviours and suggest that users do const s = "./foo.ts"; await import(s) where they want to opt out.

The impact of that decision, per Deno authors based on the internal decision to statically compile dynamic ECMA-262 import() results in the technical fact that we can make Deno's import() consistently throw - where node, bun do not throw under the same conditions Deno dynamic import("./exports") throws module not found for "exports.js" dynamically created in the script

// Deno dynamic import("./exports") throws module not found for "exports.js" dynamically created in the script
// dynamic_import_always_throws.js
// References: https://www.reddit.com/r/Deno/comments/18unb03/comment/kfsszsw/ https://github.com/denoland/deno/issues/20945
// Usage:
// deno run -A dynamic_import_always_throws.js
// bun run dynamic_import_always_throws.js
// node --experimental-default-type=module dynamic_import_always_throws.js
import { open, unlink } from "node:fs/promises";
const runtime = navigator.userAgent;
const encoder = new TextEncoder();
try {
  const script = `export default 1;`;
  // deno
  if (runtime.includes("Deno")) {
    await Deno.writeFile("exports.js", encoder.encode(script));
  }
  // node
  if (runtime.includes("Node")) {
    const dynamic = await open("./exports.js", "w");
    await dynamic.write(script);
    await dynamic.close();
  }
  // bun
  if (runtime.includes("Bun")) {
    await Bun.write("exports.js", encoder.encode(script));
  }
  const { default: module } = await import("./exports.js"); // Raw string specifier
  console.log({ module });
  console.log({ runtime });
} catch (e) {
  console.log("Deno always throws.");
  console.log({ runtime });
  console.trace();
  console.log(e.stack);
} finally {
  console.log("Finally");
  // node, bun
  if (runtime.includes("Node") || runtime.includes("Bun")) {
    await unlink("./exports.js");
  } // deno
  else if (runtime.includes("Deno")) {
    await Deno.remove("./exports.js");
  }
}

Now, let's examine the ECMA-262 language for dynamic import() re specifiers. I don't see anywhere here [13.3.10.1 Runtime Semantics: Evaluation ImportCall : import ( AssignmentExpression or 13.3.10.1.1 ContinueDynamicImport ( promiseCapability, moduleCompletion ) in the steps at where dynamic import("./path") inside a running script should throw, per the specification. Some might disagree and claim that runtimes are free to do or not do whatever they want for dynamic import() specifiers.

Deno is only doing this, per the above Deno issue, in order to accomodate TypeScript static compilation for Deno-specific bahaviours. The effecttive result is Deno's implementation of ECMA-262 dynamic import() is not dynamic.

Deno does support WICG Import Maps. Node.js doesn't. Import Maps won't help when we are deliberately running dynamic import() inside an active script.

So TypeScript folks have to reconcile that you can't have a completely static scripting language and support dynamic scripting.

Since TypeScript folks are interested solely in static scripting, bring WICG Import Maps into TypeScript, instead of some custom TypeScript solution that departs further from the dynamic JavaScript programming language.

I'll leave it to the reader to determine whether or not Deno is in conformance with ECMA-262.

nicolo-ribaudo commented 4 months ago

The first bullet point of https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-HostLoadImportedModule ("or a throw completion") is what makes Deno compliant. ECMA-262 doesn't have a concept of URLs, paths, and file systems at all: it's einteirely implementation-defined.

guest271314 commented 4 months ago

@nicolo-ribaudo

The first bullet point of https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-HostLoadImportedModule ("or a throw completion") is what makes Deno compliant.

So that necessarily means in this case Deno behaviour that throws and everybody else's implementation other than Deno that does not throw for raw string specifiers are both compliant?

See what I mean about the vagueness of ECMA-262 in this case?

nicolo-ribaudo commented 4 months ago

So that necessarily means in this case Deno behaviour that throws and everybody else's implementation other than Deno that does not throw for raw string specifiers are both compliant?

Yes

See what I mean about the vagueness of ECMA-262 in this case?

And there is a good reason for that: ECMAScript is designed to work everywhere, while I/O is platform-specific. The only I/O that ECMA-262 is aware of is datetime and randomness.

ECMAScript can run on platforms that use URLs, that use filesystem paths, or even on platforms that don't have a concept of a hierarchical filesystem. For this reason, what's the meaning of the string you pass to imports (both static and dynamic) is entirely platform-defined, and individual platforms have to decide what's the meaning of that string and if it corresponds to a module or not.

guest271314 commented 4 months ago

Yes

I have never observed any specification, standard where throwing is optional, at implemenmter discretion. Where one implementer deliberately throws in one case, other implementers do no throw and bother are conformant to the specification.

Never seen that on any specification for an actual build plan, either. Where having a 10 foot retaining wall is optional. Never seen that in law, either. Where a statute or administrative regulation has the legislative or administrative intent to fine one person, not fine another person for the same conduct or activity.

Nonetheless people will no doubt argue that one implementer can throw for impoirt("./exports.js") another implementer of the same specification can not throw for import("./exports.js") - and both are equally comformant to the same specification language.

And there is a good reason for that: ECMAScript is designed to work everywhere, while I/O is platform-specific. The only I/O that ECMA-262 is aware of is datetime and randomness.

I actually think that is an omission in ECMA-262. For every JavaScript engine and/or runtime that does implement I/O, each implementatiuon is completely different. There is zero (0) compatibility or interoperability between readline() of V8's d8 and SpiderMonkey's js, nor between Deno, Node.js, QuickJS, txiki.js, et al. https://github.com/guest271314/NativeMessagingHosts - because reading standard input stream and writing to standard output stream, and handling standard error are not specified by ECMA-262.

We have all sorts of exotic objects, realms, signals, dynamic import() that can throw and not throw and both be argued to be conformant to the same language, no basic STDIO written out. That's a huge omission for a general programming language.

Nobody seems to notice because people wind up huddling in their own little JavaScript corners, entertaining preferences, shouting whose package registry is bigger, whose been around the longest, and so forth. But try to run the same code in multiple JavaScript engines or runtimes and see what happens. It takes some work to make that so https://github.com/guest271314/NativeMessagingHosts/blob/main/nm_host.js. When it's a simple matter to specify.

But what difference does it make given since we've opened the books any implementation can throw or not throw for any section of ECMA-262. At least the Chromium folks confessed their implementation of MediaStreamTrack of kind audio is not compliant with W3C Media Capture and Streams, even if they havn't fixed that, yet. It generally takes a few years for Chromium folks to fix stuff, 6 years to get rid of censoring webkitSpeechRecognition(); 5 years to capture system audio on Linux.

TBH if I were in to TypeScript developement and I would modify or abandon that claim that TypeScript is ECMA-262 compliant, or follows the specification. The specification is clearly meaningless when people can interpret throwing and not throwing an error to be equally specification conformant.

Further, it's like Bun claiming it somehow is tryinmg to implement Node.js infrastructure, though we can't full-duplex stream using WHATWG Fwetch in Bun because there's no HTTP/2 support.

Just do your own thing TypeScript folks, without clinging to ECMA-262. Everybody else does their own thing and massages compliance out of throwing an error or not throwing an error.

guest271314 commented 4 months ago

I'll note this, I have seen such ambiguous language in statues and administrative regulations in the domain of law, re how the preceding or following words can be interpreted by jurists. It's called a term of art in law, and if you ever see this language you best pay careful attention because it generally means the legislature ran out of time, or knows there's some bs going on:

"Notwithstanding any provision to the contrary"

Anyway, good luck trying to be in conformance with ECMA-262, statically analyzing dynamic modules in TypeScript world. Cheers.

jogibear9988 commented 4 months ago

Add least adding a setting (wich default is false), like 'addFileExtensionToImportsMissingThemIfTheyAreResolvedWith' would not harm anybody. If you don't need it, keep it false

guest271314 commented 4 months ago

Add least adding a setting (wich default is false), like 'addFileExtensionToImportsMissingThemIfTheyAreResolvedWith' would not harm anybody. If you don't need it, keep it false

We already have that with WICG Import Maps. That ain't ECMA-262, but neither is WHATWG Fetch or Streams.

<script type="importmap">
  {
    "imports": {
      "test/file": "./x.json",
      "test/sub/file": "./z.json"
   }
 }
</script>
<script type="module">
  import x from "test/file"
  with {
    type: "json"
  };
  import z from "test/sub/file"
  with {
    type: "json"
  };
  console.log(
    new TextDecoder().decode(new Uint8Array([x, z])), 
    import.meta.resolve("test/file"), 
    import.meta.resolve("test/sub/file")
  );
</script>

The dynamic import() part I see no solution for in TypeScript. TypeScript folks have to massage "we can throw here if we want to so we can statically analyze dynamic modules" into the mix to support the philosophy of statically analyzing everything. Well, you can be static and dynamic at the same time. That's physically impossible. You'll have to make a decision on that. I suspect you'll adhere to Deno's interpretation that, "we can throw here, there's ambiguity or room to do that so we can support TypeScript".

djfm commented 4 months ago

Is this still a problem in practice?

I don't remember all the subtleties of this madness, but what I know is that now if I put module: "commonjs" and whatever target I want in my tsconfig.json then I can use relative imports without extension in my code and the raw output of tsc runs just fine under node.

Am I missing something important here?

EDIT: Ok I re-familiarized me with the issue. My tsc emitted code doesn't use imports that's why it works. Are we actually missing out on important stuff by not using imports in the JS code?

On Mon, 1 Jul 2024, 15:53 guest271314, @.***> wrote:

Add least adding a setting (wich default is false), like 'addFileExtensionToImportsMissingThemIfTheyAreResolvedWith' would not harm anybody. If you don't need it, keep it false

We already have that with WICG Import Maps. That ain't ECMA-262, but neither is WHATWG Fetch or Streams.

The dynamic import() part I see no solution for in TypeScript. TypeScript folks have to massage "we can throw here if we want to so we can statically analyze dynamic modules" into the mix to support the philosophy of statically analyzing everything. Well, you can be static and dynamic at the same time. That's physically impossible. You'll have to make a decision on that. I suspect you'll adhere to Deno's interpretation that, "we can throw here, there's ambiguity or room to do that so we can support TypeScript".

— Reply to this email directly, view it on GitHub https://github.com/microsoft/TypeScript/issues/50501#issuecomment-2200221442, or unsubscribe https://github.com/notifications/unsubscribe-auth/AALESEZ6TJOS4PGJNS4TZ73ZKFNPBAVCNFSM574CX7O2U5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TEMRQGAZDEMJUGQZA . You are receiving this because you were mentioned.Message ID: @.***>

guest271314 commented 4 months ago

Are we actually missing out on important stuff by not using imports in the JS code?

CommonJS is not Ecmascript Modules as defined in ECMA-262.

Node.js default loader is CommonJS. There are dozens of JavaScript runtimes that don't use CommonJS.

I'll point out that Bun supports TypeScript out of the box and does not throw as Deno does for dynamic import() with raw string specifier.

nicolo-ribaudo commented 4 months ago

Out of curiosity, why are we discussing some Deno-specific behavior here and not in the Deno repo? Especially if Bun shows that this is not a restriction from TypeScript.

guest271314 commented 4 months ago

Out of curiosity, why are we discussing some Deno-specific behavior here and not in the Deno repo? Especially if Bun shows that this is not a restriction from TypeScript.

Well, why does tsconfig.json have a module: "commonjs" option if TypeScript is allegedly ECMA-262 conformant?

CommonJS is not specified in ECMA-262.

guest271314 commented 4 months ago

Out of curiosity, why are we discussing some Deno-specific behavior here and not in the Deno repo? Especially if Bun shows that this is not a restriction from TypeScript.

Because for Deno TypeScript is a first-class citizen. And Deno has bent the interpretation of dynamic imort() in ECMA-262 to satisfay TypeScript constituents.

TypeScript has massaged CommonJS into its configuration to satisfy Node.js constituents.

So there's exceptions to rules happening everywhere here, from a neutral perspective.

I test and break multiple JavaScript engines runtimes at the same time without entertaining a preference for any. So I see what's going on.