Skip to content

Explicitly set extension on imported source files#1843

Open
peaBerberian wants to merge 1 commit into
devfrom
explicit-ts
Open

Explicitly set extension on imported source files#1843
peaBerberian wants to merge 1 commit into
devfrom
explicit-ts

Conversation

@peaBerberian
Copy link
Copy Markdown
Collaborator

@peaBerberian peaBerberian commented May 5, 2026

Observation

While working on TypeScript updates on another project, I noticed a key central things that I did not consider at all in the RxPlayer: the presence of extensions on import statements (e.g. the presence of a .js / .ts for the imported script).

Our exposed non-bundled builds (imported when relying on npm + import / require statements) do not have them, I looked into why and if it may cause issues.

The specs

Having extensions on import statements is technically not an ECMAScript requirement which leaves the task of "module loading" to the host: https://tc39.es/ecma262/#sec-hostresolveimportedmodule

Here, hosts are most likely web browsers, which theoretically support ES6 import natively. Its resolution mechanism is well-defined: https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier

TL;DR: it constructs an URL and then rely on that URL. Generally, "extensions" are part of the resource name in an URL and cannot be just omitted.

Alternatively Node.js and other JS-adjacent hosts also do not have this "magic extension omission" in them, prefering an explicit path instead.

Is it an issue?

Thankfully we are saved by the fact that almost all applications rely on us with a bundler (which generally have that extension magic), or when they do not they use our bundles (which by the fact of them being bundles do not need to do import statements, removing the issue.)

So this could also be considered a non-issue, just a "technically, in niche scenarios, we're not as compatible as we could be". I put an emphasis on that because there's no actual pressure to merge this.

Possible fixes

Still, I wanted to see how we could fix this.

TypeScript is not excessively helpful here because it will not by itself determine extensions and has a few caveats linked to it.

I nonetheless noted 4 possible solutions:

  1. We leave our build as is

  2. Our dist/es2017 and dist/commonjs builds are now also bundles (e.g. hls.js does this). This removes the extension issue but leave multiple other questions (regarding downleveling ES versions, tree-shaking, compatibility with previous versions etc.).

  3. We from now on force the usage of extensions on import statements in the source file (e.g. import X from "../adaptive/ becomes import X from "../adaptive/index.ts").

    This would work, with the addition of the relatively new (2024) rewriteRelativeImportExtensions TypeScript option, which has many caveats: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-7.html#path-rewriting-for-relative-paths

    (This are only about dynamic imports, alias etc., features we do not use for now, but to note).

    It's nonetheless my favorite for its simplicity in terms of concept: when we import, we just put the path to the file that we actually import. Really straightforward with no cognitive load and other TypeScript-compatible tools/runtimes will have no issue exploiting it.

  4. The solution historically advised by TypeScript, which is to do like 3, but indicating the destination filename instead of the source (here: .js instead of .ts, more simply put).

    The nice thing is that it then means less work for TypeScript (it doesn't have to translate paths) and less caveats so all in all less bug chances.

    But there are several things I don't appreciate with this way, especially the fact that you have to think about the produced output in source files and the poor compatibility with naive tooling just analyzing path without being aware of some weirdness of TypeScript import semantics.

    Relying on the actual filename in source files also seem to be the popular way now that we have many typescript-compliant run times (see TypeScript's 62342 issue, not linking it directly here because GitHub has annoying features when direct links are written).

So I chose to start here with 3, that this commit implements.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

✅ Automated performance checks have passed on commit c604aed1226816737d66eac17c6ac33a59da624a with the base branch dev.

Details

Performance tests 1st run output

No significative change in performance for tests:

Name Mean Median
loading 25.22ms -> 26.09ms (-0.875ms, z: 1.19531) 35.85ms -> 35.85ms
seeking 239.84ms -> 226.51ms (13.338ms, z: 0.98980) 18.00ms -> 18.15ms
audio-track-reload 31.87ms -> 31.79ms (0.075ms, z: 1.11133) 46.80ms -> 46.65ms
cold loading multithread 52.35ms -> 53.05ms (-0.700ms, z: 7.74167) 77.55ms -> 76.50ms
seeking multithread 242.50ms -> 219.83ms (22.665ms, z: 1.23844) 19.35ms -> 19.50ms
audio-track-reload multithread 31.22ms -> 31.07ms (0.155ms, z: 1.89271) 45.90ms -> 45.45ms
hot loading multithread 21.68ms -> 21.61ms (0.073ms, z: 2.61403) 31.95ms -> 31.50ms

While working on TypeScript updates on another project, I noticed a key
central things that I did not consider at all in the RxPlayer:
the presence of extensions on `import` statements (e.g. the presence of
a `.js` / `.ts` for the imported script).

Our exposed non-bundled builds (imported when relying on npm + `import`
/ `require` statements) do not have them, I looked into why and if it
may cause issues.

---

Having extensions on import statements is technically not an ECMAScript
requirement which leaves the task of "module loading" to the host:
https://tc39.es/ecma262/#sec-hostresolveimportedmodule

Here, hosts are most likely web browsers, which theoretically support ES6
import natively. Its resolution mechanism is well-defined:
https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier

TL;DR: it constructs an URL and then rely on that URL. Generally,
"extensions" are part of the resource name in an URL and cannot be just
omitted.

Alternatively Node.js and other JS-adjacent hosts also do not have this
"magic extension omission" in them, prefering an explicit path instead.

---

Thankfully we are saved by the fact that almost all applications rely on
us with a bundler (which generally have that extension magic), or when
they do not they use our bundles (which by the fact of them being bundles
do not need to do `import` statements, removing the issue.)

**So this could also be considered a non-issue**, just a "technically,
in niche scenarios, we're not as compatible as we could be". I put an
emphasis on that because there's no actual pressure to merge this.

---

Still, I wanted to see how we could fix this.

TypeScript is not excessively helpful here because it will not by itself
determine extensions and has a few caveats linked to it.

I nonetheless noted 4 possible solutions:

1. We leave our build as is

2. Our `dist/es2017` and `dist/commonjs` builds are now also bundles
   (e.g. `hls.js` does this). This removes the extension issue but
   leave multiple other questions (regarding downleveling ES versions,
   tree-shaking, compatibility with previous versions etc.).

3. We from now on force the usage of extensions on import statements in
   the source file (e.g. `import X from "../adaptive/` becomes `import
   X from "../adaptive/index.ts"`).

   This would work, with the addition of the relatively new (2024)
   [`rewriteRelativeImportExtensions`](https://www.typescriptlang.org/tsconfig/#rewriteRelativeImportExtensions)
   TypeScript option, which has many caveats:
   https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-7.html#path-rewriting-for-relative-paths

   (This are only about dynamic imports, alias etc., features we do not
   use for now, but to note).

   It's nonetheless my favorite for its simplicity in terms of concept:
   when we import, we just put the path to the file that we actually
   import. Really straightforward with no cognitive load.

4. The solution historically advised by TypeScript, which is to do like
   3, but indicating the destination filename instead of the source (here:
   `.js` instead of `.ts`, more simply put).

   The nice thing is that it then means less work for TypeScript (it
   doesn't have to translate paths) and less caveats so all in all less
   bug chances.

   But there are several things I don't appreciate with this way,
   especially the fact that you have to think about the produced output
   in source files and the poor compatibility with naive tooling just
   analyzing path without being aware of some weirdness of TypeScript
   import semantics.

So I chose to start here with 3, that this commit implements.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

✅ Automated performance checks have passed on commit d88d477ae5c3cae514a94791a280ee5933d526c3 with the base branch dev.

Details

Performance tests 1st run output

No significative change in performance for tests:

Name Mean Median
loading 23.01ms -> 23.07ms (-0.059ms, z: 1.04879) 32.70ms -> 32.70ms
seeking 512.97ms -> 511.63ms (1.338ms, z: 0.10027) 1514.25ms -> 1514.40ms
audio-track-reload 30.68ms -> 30.55ms (0.123ms, z: 0.52951) 45.30ms -> 45.30ms
cold loading multithread 50.59ms -> 49.11ms (1.482ms, z: 13.41777) 73.95ms -> 72.75ms
seeking multithread 238.12ms -> 277.31ms (-39.190ms, z: 0.27889) 16.95ms -> 16.65ms
audio-track-reload multithread 30.09ms -> 29.92ms (0.178ms, z: 1.63903) 44.55ms -> 44.40ms
hot loading multithread 19.85ms -> 19.13ms (0.723ms, z: 2.74126) 28.20ms -> 28.05ms

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant