I got a package.json where I export different scripts using the exports field.
"exports": {
".": {
"default": "./dist/main.es.js",
"require": "./dist/main.cjs.js",
"types": "./dist/main.d.ts"
},
"./utils": {
"default": "./dist/utils.es.js",
"require": "./dist/utils.cjs.js",
"types": "./dist/utils.d.ts"
},
"./segments/*": {
"default": "./dist/webvtt/segments/*.es.js",
"require": "./dist/webvtt/segments/*.cjs.js",
"types": "./dist/webvtt/segments/*.d.js"
}
}
The file structure is as follow
dist
├── main.cjs.js
├── main.d.ts
├── main.es.js
├── utils.cjs.js
├── utils.d.ts
├── utils.es.js
├── vite.svg
├── vtt.cjs.js
├── vtt.d.ts
├── vtt.es.js
└── webvtt
├── segments
├── Comment.cjs.js
├── Comment.d.ts
├── Comment.es.js
├── Cue.cjs.js
├── Cue.d.ts
├── Cue.es.js
├── Header.cjs.js
├── Header.d.ts
├── Header.es.js
├── Segment.cjs.js
├── Segment.d.ts
├── Segment.es.js
├── Style.cjs.js
├── Style.d.ts
└── Style.es.js
In VSCode, it now shows the utils
and segments
as exported paths
However, when importing scripts from segments
it doesn’t show which scripts I can import from that path.
But if I continue, and import any of the scripts from the segments folder, it works fine.
How can I make the IntelliSense show me the scripts I can import from the segments
path?
The repo containing the source can be found here
2
Answers
Your project’s configuration is not in harmony with itself
Meaning, you have set certain fields in both your
package.json
&tsconfig.json
files that conflict with other settings within those same files. The most notable was the way you have configured your project to resolve modules, which I’ll explain.Like many other package maintainers, you have opted to add modular support for both "ECMAScript Modules" (aka ESM) as well as "Common-JS Modules" (aka CJS). Also like many package maintainers, your failing to configure your package such that both modules exist harmoniously with one another, to be more specific: The way that you have configured module resolution for TS (in other words, the
tsconfig.json
field"moduleResolution"
) is different from how Node.js is currently being configured to resolve modules in yourpackage.json
configuration.You also have not explicitly defined a build for both ESM & CJS, furthermore; you have no entry point defined for your module when it is being consumed via an import, nor for when it is consumed via a CJS
require()
method.NOTE: I wouldn’t bombard you if it were not necessary for you to fix your package. I have spent 2 – 3 hours writing this in hopes to share what took me months to learn.
Module Loaders
So its important to just clarify what Module loaders are, and specifically what "Module Loaders are to Node.js". Module loaders are the syntax used by developers to indicate the want a certain resource to be consumed. Node.js understands two different loaders, both of which you are probably familiar with.
NOTE: I am using Node’s file-system
fs
library, simply for example purposes only. I chosefs
simply because it is familiar to many.#1 — The ECMAScript Module Loader
import * as fs from 'node:fs';
Its important to note that this loader is not only syntactically different, but it is also mechanically different as it is asynchronous.
#2 — And the CommonJS module loader:
const fs = require('node:fs');
You probably were able to infer that the
require
loader is synchronous.That asynchronous, and synchronous difference between the two loaders makes the two modules incompatible in most situations (any exceptions are beyond the topics that we are covering).
Your
package.json
fileNow, with the above said, lets look at your package.json file.Inside of your
package.json
file you have set the"type"
field as shown bellow.Setting
"type"
as"module"
in and of itself is not a bad thing, however, the way you set this field will affect the way that Node.js handles your emitted JavaScript project (and really, the JS files are the actual mechanics right? TS is more or less just a statically typed blue print in a sense). Looking at it that way, you can easily see that it is extremely important to be aware of how Node.js is working. It is important that you configure TypeScript to work in harmony with node, or else you will experience undesired results, like "your intellisense not working as expected".Its important that these 4 pertinent points are understood
"type"
field to"module"
causes Node to treat.js
files as ESM JavaScript"type"
to "commonjs", or omitting it altogether, will configure node to treat.js
files as CommonJS JavaScript..mjs
files are always loaded as ESM, despite the value of the nearestpackage.json
file’s "type" field..cjs
files are always loaded as CommonJS, despite the value of the nearestpackage.json
file’s "type" field.Configuring TypeScript’s Module Resolution
There is more than one mistake in your configuration, however, the most notable, and easiest to fix, is the value you set for the
tsconfig.json
field "moduleResolution". Your "moduleResolution" field, is currently configured as shown bellow.tsconfig.json
to["node"][1]
, as can be seen below…It should be noted that setting
"moduleResolution"
changes (in part) the resolution strategy used by TypeScript. In your case, you set it to"node"
, which tells TS to mimic Node’s CommonJS module resolution algorithm`.Here is where we get to the good stuff
So, in other words, if you have your
package.json
‘s"type"
field set to "module", your telling node.js to resolve ".js" files as ECMAScript modules, which as stated above, is an Asynchronous way of resolving modules.And if you have set
"moduleResolution"
in yourtsconfig.json
file to"node"
, you are telling TypeScript to mimic the algorithm used by "commonjs" modules, which is a synchronous resolution strategy that follows a completely different spec than ESM.Do you see how your project is not configured to resolve modules harmoniously?
I don’t know all the details on how Intellisense works for TypeScript in VS Code, but I do know it involves a language server, which is powered by TSC itself, and the way that TSC is configured to resolve modules is going to have an affect on the way Intellisense behaves when importing & exporting files. It won’t break IntelliSense, but if a resource cannot be resolved in an import statement, IntelliSense is going to show you a list without the "whatever it is" your looking for.
Also, I don’t know why your exporting your types, that is also going to affect things. You should export your types by setting the "types" field in your package.json file.
according to TypeScript documentation:
NOTE: "
"types"
has an alias, which is"typings"
"You want to set the two like this:
Configuring your Exports a bit Differently Maybe?
I do the follwoing
Build Directories
CJS
"builds/CommonJS"
ESM
"builds/ECMAScript"
Exported Entry Points
CJS
"./builds/CommonJS/main.cjs"
ESM
"./builds/ECMAScript/main.mjs"
Source Directory Tree
Emits to the following build tree
The
CommonJS
&ECMAScript
directories both contain apackage.json
, that doesn’t include the projects base-dir, when including the base package.json file, the project has 3 package.json files all together. You can get by with only 2, but I like 3, it makes the configuration more robust, and harder to break when changes are introduced to the projects file-structure.The package.json files in the
CommonJS
&ECMAScript
directories are simple — very very simple — they contain only what is absolutely needed.And just FYI, I am showing you the package.json files because it looks like your not defining a commonjs module anywhere, which can only be done using a
package.json
file, and each module type needs to be defined by its own package.json file. I see that you set a CJS entry, and have CJS files, but no explicit configuration pointing to a CJS module which is also enough to BREAK YOUR INTELLISENSE (i used bold because it answers the direct question).The extra package.json files in the build I showed you using file trees looks like this.
Project’s base
package.json
fileThere is only one ".mts" file & one ".cts" file, the rest are simply ".ts". So long as I define the project’s modules correctly, so that the same algorithm is being used by typescript & node.js, everything works harmoniously. I can import from the same files to main.cts & main.mts without maintaining seperate cts & mts versions of the same file. One source, and 2 builds.
TypeScript Configuration
You have to configure typescript to build a CJS build, and to build an ESM build. There are several different concepts that all achieve the samething, but they each look & work very differently from each other. The most simple way in my opinion takes advantage of newer TS 3.X features that were introduced for the purpose of emitting multiple builds, henceforth, the
$ tsc --build
command + flag.In a TSConfig you just want to
reference
the two different builds.then use two other tsconfig files for the builds.
Like this:
Believe it or not, I removed all the type checks and other stuff not required to get all intellisense features supported that I could, and emit a dual esm & cjs project.
Anyways, hopefully this helps
I want to point my
package.json
exports['.'].import
key at mylib
directory (where my source lives) and have done with it.That works just fine, except that Intellisense breaks in VS Code.
This happens because VS Code is parsing
package.json
to figure out where your typedefs are, and apparently VS Code has not yet caught up to the new export-mapping syntax!Remember the
main
key? It still works, and if you use it alongsideexports
, Node.js will just ignore it. But VS Code will pick it up and use it to power your Intellisense!For example: