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 your package.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 chose fs
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
file
Now, 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.
{
"type": "module"
}
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
- Setting the
"type"
field to "module"
causes Node to treat .js
files as ESM JavaScript
- When setting
"type"
to "commonjs", or omitting it altogether, will configure node to treat .js
files as CommonJS JavaScript.
- Then of course
.mjs
files are always loaded as ESM, despite the value of the nearest package.json
file's "type" field.
- And
.cjs
files are always loaded as CommonJS, despite the value of the nearest package.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...
"compilerOptions": {
"moduleResolution": "node"
}
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 your tsconfig.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:
TypeScript will use a field in package.json named types
to mirror the purpose of "main" - the compiler will use it to find the “main” definition file to consult.
NOTE: ""types"
has an alias, which is "typings"
"
You want to set the two like this:
// "tsconfig.json"
{
"compilerOptions": {
"declaration": true,
"declarationDir": "./types"
}
}
// "package.json"
{
"types": "./types"
}
Configuring your Exports a bit Differently Maybe?
I do the follwoing
Build Directories
Module Type |
FilePaths |
CJS |
"builds/CommonJS" |
ESM |
"builds/ECMAScript" |
Exported Entry Points
Module Type |
FilePaths |
CJS |
"./builds/CommonJS/main.cjs" |
ESM |
"./builds/ECMAScript/main.mjs" |
Source Directory Tree
.
├── lib
│ └── sum-lib.ts
|
├── main.cts
└── main.mts
Emits to the following build tree
.
├── CommonJS
│ ├── lib
│ ├── main.cjs
│ └── package.json <-------- Notice the package.json?
|
└── ECMAScript
├── lib
├── main.mjs
└── package.json <-------- Notice the package.json?
The CommonJS
& ECMAScript
directories both contain a package.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.
// ECMAScript/package.json
{
"type": "module"
}
// CommonJS/package.json
{
"type": "commonjs"
}
Project's base package.json
file
/*
> "package.json"
> I left out dependencies, repo url, and other things, and left the
important settings */
{
"name": "rgb-interface",
"author": "Andrew Chambers <[email protected]>",
"version": "0.0.3",
"type": "module",
"types": "types",
"exports": {
"import": "./builds/ECMAScript/main.mjs", // ESM Entry
"require": "./builds/CommonJS/main.cjs" // CJS Entry
},
}
}
// I didn't leave out "main", I just don't define it, as exports defines my entry points"
There 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.
{
"files": [],
"references": [
{ "path": "tsconfig.esm.json" },
{ "path": "tsconfig.cjs.json" }],
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": ".Cache/tsc.buildinfo.json",
// "listEmittedFiles": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"listEmittedFiles": true
}
}
then use two other tsconfig files for the builds.
Like this:
// "tsconfig.esm.json"
{
"files": [
"src/main.mts",
"src/test/esm-color-format.test.mts",
"src/lib/ansi-static.ts",
"src/lib/color-log.ts",
"src/lib/ansi.ts"
],
"exclude": ["node_modules", "**/*.cts"],
"compilerOptions": {
// ECMAS Module + ES2021 + Node-16LTS
"target": "ES2021",
"module": "Node16",
"moduleResolution": "Node16",
"esModuleInterop": false,
// STRUCTURE
"outDir": "builds/ECMAScript",
"declarationDir": "types",
"rootDir": "src",
"sourceRoot": "src",
"composite": true, // <-- must be on
// EMISSIONS
"declaration": true,
"declarationMap": true, // maps add extra intellisense features
"sourceMap": true, // both "*.d.ts.map" and "*.js.map" files
"inlineSources": true,
"noEmitOnError": true,
"noEmit": false, // Can be used when you want to emit only one build
}
// tsconfig.cjs.json
{
"files": [
"src/main.cts",
"src/test/cjs-color-format.test.cts",
"src/lib/ansi-static.ts",
"src/lib/color-log.ts",
"src/lib/ansi.ts"
],
"exclude": ["node_modules", "**/*.mts"],
"compilerOptions": {
// CommonJS + Node-8 + ES5 (Early Support)
"target": "ES5",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
// STRUCTURE
"outDir": "builds/CommonJS",
"declarationDir": "types",
"rootDir": "src",
"sourceRoot": "src",
"composite": true,
// EMISSIONS
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"inlineSources": true,
"noEmitOnError": true,
"noEmit": false,
}
}
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