skip to Main Content

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

enter image description here

However, when importing scripts from segments it doesn’t show which scripts I can import from that path.

enter image description here

But if I continue, and import any of the scripts from the segments folder, it works fine.

enter image description here

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

https://github.com/codeit-ninja/js-vtt

2

Answers


  1. 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

    1. Setting the "type" field to "module" causes Node to treat .js files as ESM JavaScript
    2. When setting "type" to "commonjs", or omitting it altogether, will configure node to treat .js files as CommonJS JavaScript.
    3. Then of course .mjs files are always loaded as ESM, despite the value of the nearest package.json file’s "type" field.
    4. 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


    Login or Signup to reply.
  2. I want to point my package.json exports['.'].import key at my lib 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 alongside exports, Node.js will just ignore it. But VS Code will pick it up and use it to power your Intellisense!

    For example:

    {
      ...,
      "exports": {
        ".": {
          "import": "./lib/index.js", // where I write my code
          "require": "./dist/default/lib/index.js" // Babel transpiler target
        }
      },
      "main": "./lib/index.js", // where VS code looks for Intellisense
      ...
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search