node.js - Typescript-paket som levereras med .mjs och .d.ts, men utan .d.mts - hur importerar man med ESM aktiverat?


Introduktion

Med stöd för ECMAScript-moduler tillagt i Typescript 4.7 kan flera nya filändelser bli involverade under en TS-kompilering, inklusive .mjs, .d.mts. Om ett projekt har detta aktiverat har TS-kompilatorn mer komplexitet att hantera när den gör modulupplösning (lokaliserar filerna att importera). Med de nya ESM-filändelserna finns det två enkla typer av moduler:

  1. en modul har .js implementation, .d.ts deklarationsfil
  2. en modul har .mjs implementation, .d.mts deklarationsfil

Fråga

Inte alla paket passar in i ovanstående kategorier. Vissa paket skickas med både .js och .mjs versioner av implementationen men bara .d.ts deklarationsfilen, utan .d.mts

Vilka är upplösningsreglerna i detta fall? Det verkar som att .mjs får prioritet över .js men vägrar fungera utan .d.mts, vilket är problematiskt om du inte äger den importerade modulen. Kan detta lösas utan att ändra paketet?


Exempel

För ett projekt som har ESM aktiverat via följande konfigurationer

// package.json
"type": "module"

// tsconfig.json
"module": "Node16",
"moduleResolution": "node16"

det beror på ett paket (t.ex. js-base64) som levereras med .js, .mjs, .d.ts men ingen .d.mts

ls -l node_modules/js-base64
base64.d.ts
base64.js
base64.mjs

sedan när jag försöker importera det som

// myfile.ts
import { Base64 } from 'js-base64'

Jag får ett fel:

Kunde inte hitta en deklarationsfil för modulen 'js-base64'. '/myproj/node_modules/js-base64/base64.mjs' har implicit typen 'any'

Men om jag gör

ln -s node_modules/js-base64/base64.d.ts node_modules/js-base64/base64.d.mts

då försvinner felet vilket antyder för mig att .d.ts medvetet ignoreras.

Svar #1

Få framtida läsare att förstå på samma sätt

Frågan ovan handlar om en package.json-fil, som AToW (vid tiden för skrivandet) var konfigurerad så här.

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"
  },
}

Problemet:


När frågans författare skulle importera paketfilen som en ESM-modul, skulle han använda ett importuttryck för att försöka komma åt den, men hans "TypeScript Compiler" (även kallad TSC) skulle kasta felet (visas nedan) varje gång han försökte använda den importerade modulen.

Kunde inte hitta en deklarationsfil för modulen: "js-base64" '/myproj/node_modules/js-base64/base64.mjs' har implicit typen 'any'

För att upprepa frågan


Kan modulen lösa sig utan att omkonfigurera paketet? Och varför verkar det som att skapa en symbolisk länk, som visas i Sergeys fråga, åtgärda det fel som uppstår?

Frågan i större detalj


Detta är ett bi-modulärt Node-paket, vilket betyder att det innehåller två byggversioner, en CJS-bygg och en ESM-bygg. Det är konfigurerat för att använda olika filer endast vid modulens ingångspunkt, resten av paketet bör (i teorin) vara samma.

Filerna som är konfigurerade för de två olika ingångspunkterna kan ses i package.json utdraget som jag postade högst upp i detta svar.

Innan vi går för långt vill jag göra det klart att konfigurationen av package.json-filen har ett enda problem, men i stort sett är den korrekt konfigurerad. ESM & CJS ingångspunkter och byggprocesser fungerar alla bra, det enda problemet är att när en TypeScript-användare importerar modulen som ett ECMAS-modul — alias ESM — så löses inte typningen, vilket leder till att TypeScript-kompilatorn genererar ett type-fel när frågeställaren försöker importera och använda paketet.

Den nuvarande konfigurationens konsekvenser

Att inte lösa med som en typad modul har några uppenbara konsekvenser. Även om de är uppenbara för de flesta, är det bra att täcka dem bara för att säkerställa att alla läsare är på samma punkt i diskussionen.

  1. Om du använder ren JavaScript kommer du troligen inte att märka att något är fel. JS använder inte typer, och därför bör importering av paketet till en ren JavaScript-kodbasis kunna göras — som ett "ES-modul", eller som ett "Common-JS modul" — utan att uppleva några problem.

  2. De som använder TypeScript bör kunna lösa modulen via en import { ... } from 'js-base64' och/eller ett require('js-base64')-uttryck och allt bör lösas med typning.

  3. De som försöker importera paketet med TypeScript — i en ESM-miljö — kommer att få typefel, och de bör märka att paketet inte löses med typer.

Varför kommer inte typerna att lösas? Är de inte inkluderade i base64.d.ts-filen?

Så, paketet är skrivet i TypeScript, generellt sett kommer ett bi-modulärt paket att vara skrivet i TypeScript, eller åtminstone transpilerat med någon transpiler. I det här fallet är paketet definitivt ett TS-skrivet paket. Jag har inte inspekterat dess kod noggrant, men jag skulle inte bli förvånad om det tidigare var ren JS och vid någon tidpunkt konverterades till TS. Hur som helst, det har faktiskt en korrekt TSC-genererad .d.ts deklarationsfil.

Så varför kommer det inte att lösas?

Det löses inte på grund av hur det har konfigurerats. Paketet är standardmässigt konfigurerat för att lösas som en "Common-JS-modul". Bas-package.json-filen innehåller inte ett "type"-fält, därför används som standard en CJS-modul. package.json-filens "type"-fält kan anges till antingen...

  1. "commonjs" (även kallat CJS) eller,
  2. "module" (även känt som ESM)
Som nämnts ovan resulterar utelämnandet av fältet "type" i att paketet löses som en CJS-modul.

Att konfigurera moduler för ESM, särskilt när man försöker stödja CJS samtidigt, har blivit en mycket komplex process och är för närvarande inte förstådd av många när det gäller paket skrivna i TypeScript. Detta beror på att mycket av stödet för ESM är nytt, trots att ESM-standarden inkluderades i ECMA-262-specifikationen redan 2015.

Det viktiga att notera är att paketunderhållaren använde paket.json-filens fält "exports" för att definiera separata ingångspunkter för de två modultyperna.

Detta är den här biten av JSON-kod:
  "types": "base64.d.ts",
  "exports": {
    ".": {
      "import": "./base64.mjs", // ESM entry point
      "require": "./base64.js" // CJS entry point
    },
    "./package.json": "./package.json"
  },

Han hade kunnat göra det tvärtom, men det gjorde han inte, han gjorde det på det sätt du ser ovan.

Hur en lösning ser ut


Problemet kan ses i utdraget ovan, fältet types är där han berättar för TSC var deklarationsfilen finns när paketet importeras som en modul, problemet är, som jag påpekade ovan, att paketet standardmässigt löses som en CJS-modul, trots att det definierar en ESM-ingångspunkt.

För att lösa problemet behöver paketet ha en deklarationsfil som har exakt samma namn och filändelse som filen som är angiven som ESM-entrépunkt. Ett annat sätt att lösa problemet är att den redan existerande base64.d.ts-deklarationsfilen behöver konfigureras explicit för att lösas med filen som är angiven som ESM-entrépunkt, (vilket är base64.mjs).

Jag gick in och justerade saker, för att verifiera att det jag skriver just nu är kanon. En av de allra första sakerna jag gjorde när jag experimenterade med paketet var att ändra konfigurationsinställningen i package.json-filens "exports"-fält.

// 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"
    },
  },
}

Ovan kan du se att paketet är konfigurerat så att TypeScript nu kan härleda (från fältet "types" som är inbäddat i fältet "exports") att filen base64.d.ts ska lösas med alla exports.

Han kunde ha skrivit exports-fältet så här:
Nedan är ett utdrag jag tog från "TypeScript v4.7 Release Notes", kommentarerna i utdraget är OTROLIGT HJÄLPSAMMA. Försök att förstå vad kommentarerna försöker säga.
    "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",
    },
  }

Skillnaden mellan JD3V package.json-filen och TS v4.7-exemplet är att "import"-fältet (i "exports"-fältet) tar ett objekt som sitt tilldelade värde, vilket innebär att när types-platsen är inställd i "TS v4.7" anger den en specifik fil för "base64.mjs", medan den i JD3V-exemplet anger en specifik "types"-plats för alla exports.

VÄNLIGEN OBSERVERA: "Som paketet är för närvarande, med användning av den konfiguration som visas högst upp i detta svar, kan denna modul inte importeras som en ESM-modul i ett TypeScript-projekt utan att åtgärda hur filen 'base64.d.ts' löses. För närvarande löses den inte när paketet importeras som en ESM-modul."
Dessutom är allt detta officiellt dokumenterat och kan hittas på de två länkarna nedan:

TS v4.7: "ECMAScript-modulstöd i Node.js"

TS-Lang: package.json Exports, Imports och Självreferenser

Relaterade artiklar