Getting Future Readers on the Same Page
The question above asks about a package.json file, that AToW (at the time of writing) was configured like this.
js-base64/package.json:
{
"main": "base64.js",
"module": "base64.mjs",
"types": "base64.d.ts",
"files": [
"base64.js",
"base64.mjs",
"base64.d.ts"
],
"exports": {
".": {
"import": "./base64.mjs",
"require": "./base64.js"
},
"./package.json": "./package.json"
},
}
The Issue:
When the question's author would import the package file as an ESM module, he would use an import statement to attempt to access it, however, his "TypeScript Compiler" (aka TSC) would throw the error (displayed below) each time he tried to use the imported module.
Could not find a declaration file for module: "js-base64" '/myproj/node_modules/js-base64/base64.mjs' implicitly has an 'any' type
To Reiterate the Question
Can the module resolve without re-configuring the package. And why does creating a symbolic link, as shown is Sergey's question, seem to fix the error that is occurring?
The Issue in Greater Detail
This is a bi-modular Node package, meaning that it contains two builds, a CJS build, and a ESM build. It is configured to use different files only at the entry-point of the module, the rest of the package should (in theory) be the same.
The files configured for the two different entry points can be seen in the package.json snippet I posted at the very-top of this answer.
Before we get to far, I want to make it clear, the package.json files configuration has a single issue, however, for the most part, its configured correctly. The ESM & CJS entry points & builds all work well, the only problem is that when a TypeScript user imports the module as an ECMAS-Module — aka ESM — it doesn't resolve typed, consequently, the TypeScript compiler throws a type error when the questions author attempts to import & use the package.
The Current Configuration's Implications
Not resolving with as a typed module has some obvious implications. Although, they are obvious to most, its good to cover them just to make sure all readers are grounded at the same point in the discussion.
If your use pure JavaScript you likely won't notice anything is off. JS doesn't use types, and therefore, importing the package into a pure JavaScript code-base, should be able to be done — as an "ES-Module", or as a "Common-JS Module" — without experiencing any issues.
Those using TypeScript should be able to resolve the module via an import { ... } from 'js-base64' &/or a require('js-base64') statement and everything should resolve being typed.
Those trying to import the package using TypeScript — in an ESM environment — will get type errors, and they should notice that the package is not resolving with types.
Why won't the Types Resolve? Are they not included in the base64.d.ts file?
So, the package is written in TypeScript, generally a bi-modular package will be written in TypeScript, or at the very least, transpilled using some transpiler. In this case, the package is defiantly a TS authored package. I havn't inspected its code thoroughly but I wouldn't be surprised if it use to be pure JS, and was converted at some point to TS. Either way, it does have a properly TSC emitted .d.ts declaration file.
So why won't it resolve?
It isn't resolving because of how it has been configured. The package is configured, by default, to resolve as a "Common-JS Module". The base package.json file doesn't contain a "type" field, therefore it defaults to a CJS module. The package.json file's "type" field can be set to either...
"commonjs" (aka CJS) Â or,
"module" (aka ESM)
As stated above, omitting the "type" field results in the package resolving as a CJS module.
Configuring modules for ESM, especially when attempting to support CJS at the same time, has become a very complex ordeal, and currently is not understood by many when it pertains to TypeScript written packages. This is because much of the support for ESM is new, despite the ESM standard being included in the ECMA-262 specification back in 2015.
The important thing to note is that the package-maintainer used the package.json file's "exports" field to define separate entry points for the two module types.
That is this bit of JSON-Code here:
"types": "base64.d.ts",
"exports": {
".": {
"import": "./base64.mjs", // ESM entry point
"require": "./base64.js" // CJS entry point
},
"./package.json": "./package.json"
},
He could have done it the other way around, but he didn't, he did it the way you see above.
What a Solution Looks Like
The problem can be seen in the snippet above, the types field is where he tells TSC where the declaration file is when the package is imported as a module, the problem is, as I pointed out above, the package is defaulting to resolve as a CJS module, despite defining an ESM entry point.
To solve the issue, the package needs to have a declaration file that has the same exact name & file-extension as the file set as the ESM entry point. Another way to solve the issue or the already existing base64.d.ts declaration file needs to be explicitly configured to resolve with the file set as the ESM entry point, (which is base64.mjs).
I went in and adjusted things, to verify that what I am writing write now is cannon. One of the very first things I did when playing around with the package, was change the configuration set in the package.json file's "exports" field.
##### This is what the "exports" field should look like
// jD3V's adjusted package configuration
{
"main": "base64.js",
"module": "base64.mjs",
"types": "base64.d.ts",
"files": [
"base64.js",
"base64.mjs",
"base64.d.ts"
],
"exports": {
".": {
"import": "./base64.mjs",
"require": "./base64.js",
"types": "./base64.d.ts"
},
},
}
Above you can see that the package is configured such that TypeScript can now infer (from the "types" field that's wrapped in the "exports" field) that the base64.d.ts file should be resolved with all exports.
He could have wrote the exports field like this:
Below is a bit I took from "TypeScript v4.7 Release Notes", the comments in the snippet are EXTREMELY HELPFUL. Please try to understand what the comments are trying to say.
"exports": {
".": {
// Entry-point for `import "my-package"` in ESM
"import": {
// Where TypeScript will look.
"types": "./base64.d.ts",
// Where Node.js will look.
"default": "./base64.mjs"
},
"require": "./base64.js",
},
}
The difference between the JD3V package.json file, and the TS v4.7 snippet, is that the "import" field (in the "exports" field) takes an object as its assigned value, consequently; when the types location is set in the "TS v4.7" it is setting a specific file for "base64.mjs", where as in the JD3V snippet it sets a specific "types" location for all exports.
PLEASE NOTE: "As the package stands, using the configuration shown at the top of this answer, this module cannot be imported as an ESM module, into a TypeScript project, without fixing how the 'base64.d.ts' file resolves. As it currently doesn't resolve when the package is imported an ESM module."
Furthermore, this is all officially documented, and can be found at the 2 links below:
import { Base64 } from 'js-base64'doesn't make a lot of sense. You should have to have added a file extension to it. Also, you shouldn't have two different file-extensions in the same project unless your project is configured as being able to be implemented as 2 different module types. (Soft linking the file w/differnt ext is adding another type)js-base64package, and tried it in an ESM module that I am working on, and the issue was easy enough to recreate. The problem is, he doesn't include a js-base64.d.mts file, and he has too, because he is using the "*.mjs" file type.