Support Typescript 4.7 "Node16" extension requirements
Description
With Typescript version 4.7, Typescript now has the option to preserve "ES Modules" if tsconfig.json specifies:
{
"compilerOptions": {
"module": "Node16"
}
}
This is a step forward in supporting native ESM in Node from Typescript's direction, but it comes with the requirement that all typescript modules when imported use the .js extension.
Ref: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html
This works all fine and well in Vite when importing typescript modules in .ts-files.
The problems begin when importing .ts-files in .js, .vue, .svelte, and so on.
E.g.
// ./some-ts-file.ts
export const hello = "Hello";
// ./my-app.js
import { hello } from "./some-ts-file.js"
See reproducible example: https://github.com/aMediocreDad/vite-import-ts-as-js
Suggested solution
The expectation is that importing .ts-files as .js resolves the file like it does in .ts-files at the moment (See related "Closed" issue: https://github.com/vitejs/vite/issues/3040).
Alternative
No response
Additional context
No response
Validations
- [X] Follow our Code of Conduct
- [X] Read the Contributing Guidelines.
- [X] Read the docs.
- [X] Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
Out of curiosity, did you notice improvements? Runtime or Buildtime? This is going the Deno route, of requiring extensions in the imports, which always irked me, but I can learn.
Out of curiosity, did you notice improvements? Runtime or Buildtime?
I have not tested for any build/runtime improvements. However, it does provide some long awaited developer experience improvements.
Using the "Node16" module specifier makes Typescript finally use the import/export fields in package.json properly. This is a big help where packages don't use the "main" field because they do not want to indicate backwards compatibility with pre Node 12. Previously, Typescript tooling was usually difficult to set up because of how it failed to recognize newer package.json configurations (that only cater to ESM).
I do not use the tsc compiler so I won't see the improvements that the compilation options provide. But the steady move towards proper ESM handling across all tools commonly used in the same toolchain is all I really want to see.
Thanks for explaining that, appreciate it.
Running into another issue with Subpath import patterns
We can't safely use #src as defined in package.json. In bigger projects this is really helpful.
Vue project that doesn't run because of Vite... https://stackblitz.com/edit/but-subpath-imports-vite?file=package.json%3AL11
Another example, showing tsx running correctly, but vite failing dude to an issue with TS's Node16 module loading support. Ts vite project, runs fine in TSX
Resolved TypeScript issue supporting Subpath Imports: https://github.com/microsoft/TypeScript/issues/44848#issuecomment-1212826554
I wouldn't call this an enhancement, but a build breaking bug. This issue has caused me to refactor several repos that made heavy use of it. Using ssrLoadModule causes some workarounds to stop working... meaning I have no choice but to refactor the relatively old and well supported Node feature out.
We've discussed this in the last team meeting and decided to support .js -> .ts resolving for JS files too based on the related tsconfig.json/jsconfig.json that has the special resolution setting.
The rule seems to be that, if the moduleResolution is node16, nodenext, or above; Or else if the module is node16, nodenext, or above; Then we'd kick in the file resolution.
As the official Vite plugins are currently packaged using a Psuedo-module format that uses export {vuePlugin as default} rather than the ESM canonical export default function vuePlugin (...
You currently have to import some plugins with import { default as vue } from '@vitejs/plugin-vue' rather than the actual default import. As the synthetic imports may not be applied to types, but if enabled, are applied to code.
Definition using synthetic legacy default export: https://unpkg.com/browse/@vitejs/[email protected]/dist/index.d.ts CF: https://stackblitz.com/~/github.com/FossPrime/bug-primevue-esm-types SBC: https://stackblitz.com/edit/public-sakai-yqyw2y?file=src%2Fmain.ts%3AL13,src%2Fvite-env.d.ts%3AL11
We've discussed this in the last team meeting and decided to support
.js->.tsresolving for JS files too based on the relatedtsconfig.json/jsconfig.jsonthat has the special resolution setting.The rule seems to be that, if the
moduleResolutionisnode16,nodenext, or above; Or else if themoduleisnode16,nodenext, or above; Then we'd kick in the file resolution.
When will this happen? Currently, my setup syntax highlights my local imports (without ext) as errors, but vite can build / serve them just fine. It's kind of annoying. Or am I missing some nuance of intent?
This "kind of" works, but requires a full restart of the vite dev server every time a new ts file is created and later on imported from another file with a js extension.
I'd like to suggest that Vite should always resolve .js to .ts in all moduleResolution modes. Both TypeScript and esbuild do this and I think Vite should do the same.
TypeScript's handbook on module theory describes its behaviour:
While the
modulecompiler option can transform imports and exports in input files to different module formats in output files, the module specifier (the stringfromwhich youimport, or pass torequire) is always emitted as-written.
There is no compiler option that enables transforming, substituting, or rewriting module specifiers. Consequently, module specifiers must be written in a way that works for the code’s target runtime or bundler, and it’s TypeScript’s job to understand those output-relative specifiers.
This also clarifies why TypeScript doesn’t modify import specifiers during emit: the relationship between an import specifier and a file on disk (if one even exists) is host-defined, and TypeScript is not a host.
TypeScript should always follow an "output-relative" import specifier (like .js or .jsx) to its corresponding source file (like .ts or .tsx), regardless of the moduleResolution value. Node16 and NodeNext don't enable this behaviour, they just enforce mandatory file extensions.
I think Vite should do the same, to gradually eliminate cases where TypeScript can resolve an import but Vite can't.
Today Vite conditionally maps import specifiers if the importer is a TypeScript file, but that's backwards; all that should matter is whether the exporter could be a TypeScript file. If an HTML file, MDX file, or any other type of file wants to reference a .ts file using a .js extension, ideally Vite would resolve this the same way TypeScript does.
If you start closing this gap, I think you start eliminating an entire category of interop problems. In other words, I couldn't agree more with @aMediocreDad:
I do not use the tsc compiler so I won't see the improvements that the compilation options provide. But the steady move towards proper ESM handling across all tools commonly used in the same toolchain is all I really want to see.