Typescript: Support for NodeJS 12.7+ package exports

Created on 26 Aug 2019  ยท  26Comments  ยท  Source: microsoft/TypeScript

NodeJS 12.7 added support for a (currently experimental) feature for custom package imports and exports in package.json: https://github.com/jkrems/proposal-pkg-exports/

In short, this feature allows a package author to redirect exports in their package to alternate locations:

{
  "name": "pkg",
  /* [...] */
  "exports": {
    "./foo": "./target.js",
    "./bar/": "./dist/nested/dir/"
  }
}

This is currently only available when --experiemental-exports is passed to NodeJS, however we should continue to track the development of this feature as it progresses.

feature-request

Most helpful comment

Any updates on this?

All 26 comments

CC: @weswigham, @danielrosenwasser

Yeah, I know - I don't think we should implement support for it till it's stabilized - things like how . works are still being discussed in the modules group.

I agree, this issue exists primarily to serve as a place for us to track the progress of this feature in NodeJS.

Given node is now at v12.11.1 and I believe v12 will enter LTS soon has this functionality stabilised enough to warrant inclusion in typescript module resolution now?

This will prove very helpful when working with yarn simlinked monorepos as the existing types field isn't enough when there are multiple files not in the package root dir.

We spoke about it in the modules wg on Wednesday - it's going to unflag with es modules as a whole (even the cjs support), so it can be a reliable fallback-allowing mechanism for pre-esm node. It... Should... Unflag during the node 12 lifetime. But that hasn't happened yet, and details are still being fleshed out~

Looks like this has unflagged in 13.2.0: https://github.com/nodejs/node/blob/v13.2.0/doc/changelogs/CHANGELOG_V13.md#notable-changes

(with support for exports)

Node 14 (next LTS) is scheduled for release 2020-04-21 and I'm guessing it will support exports unflagged as node 13 does and at that point I know I will want to use it :-). Is typescript planning to support exports in the same timeframe as the node 14 release?

Yeah, we've just been waiting for it to stabilize a bit, since it's still experimental, even if it's unflagged (it emits a warning when it's used in resolution), since we don't want to need to support an old experimental version _and_ a final version (and it does still see significant change - there's discussion about removing the main fallback right now).

Is there a way around this for the time being? Perhaps manually linking up modules via an index.d.ts?

I'm using path based modules such as @okdecm/my-package/Databases/Postgres and thus have no index to specify as my main.
The code now works using the exports option, but tooling such as Visual Studio Code still can't resolve the modules for referencing (due to current lack of TypeScript support).

It'd be ideal if there were a way to explicitly set these in the mean time.

@okdecm, try setting the baseUrl and paths. If @my/bar depends on @my/foo then you might have a tsconfig like this:

// packages/bar/tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "rootDir": "src",
    "outDir": "dist/lib",
    "composite": true,
    "paths": {
      "@my/foo": [
        "../foo/dist/lib/index"
      ],
      "@my/foo/models": [
        "../foo/dist/lib/a/b/c/d/models"
      ],
      "@my/foo/*": [
        "../foo/dist/lib/*"
      ]
    }
  },
  "references": [{ "path": "../foo/tsconfig.json" }]
}

The paths should point to the emitted file. Remember that the paths are relative to the baseUrl so it might make sense to set the baseUrl a parent folder.

One of the ideas for conditional exports was to allow things like types per exported path.

{
  "name": "pkg",
  /* [...] */
  "exports": {
    "./foo": {
      "types": "./types/foo.d.ts",
      "default": "./target.js"
    },
    "./bar/": {
      "types": "./types/bar.d.ts",
      "default": "./dist/nested/dir/"
    }
  }
}

This would be great to see in a potential TypeScript integration. :)

Any updates on this?

I'm not sure I understand what the obstacle is? I would think that TS just needs to continue looking at the types/typings field, and load it as is. It should up to the author to declare multiple module definitions within that single d.ts file. Important TS fields/paths should not be scattered throughout the package.json file โ€“ too much room for error.

I have examples of this already. I'd expect these to Just Work โ„ข๏ธ :

This would be backwards compatible too, because a definition file that _doesn't_ contain a declare module wrapper already assumes that the definition is the default / applies to the entire package.

An interesting tidbit is that if you load use a submodule inside a JS file, eg kleur/colors, within VSCode, the submodule's types are picked up and inferred correctly. Writing the same code inside a .ts file short circuits everything to any type.

import * as colors from 'kleur/colors';

colors.r
// (js) has code completions
// (ts) *crickets*

this exports field is stable in node api now
so it's the time to ts support it
any update?

Sort-of. Basic usecases are pretty stable, but a lot of conditional-related cases are still under discussion. Anyways, we've missed the mark for inclusion in 4.0, so 4.1 would be the earliest you'd see it.

Would TypeScript itself love to have similar options so we can decide the entry modules of a TypeScript project?

// Lib/tsconfig.json
{compilerOptions: {
  exports: [
    "index.ts" // You can only import the `index.ts` from project `Lib`
  ]
}}
// App/tsconfig.json
{references: [
  "path": "<path-to-project-Lib>"
]}
// App/x.ts
import "<path-to-project-Lib>"; // OK
import "<path-to-project-Lib>/hid"; // Type checking error: no such module `<path-to-project-Lib>/hid` 

Resolve (and dependents) support is being worked on
https://github.com/browserify/resolve/pull/224

Is there any in-progress work on this and/or are there roadblocks that would need solving?

With node-v14 sceduled to enter Active LTS in under three months, i'd suspect the number of people wanting to have a feature like this to rise at an increasing rate

Is there a way around this for the time being?

I'm currently writing a module composed of submodules only, it's TypeScript compiled for browser, node cjs, node esm. Using package.json conditional exports (wildcard syntax from v14.13.0).

Here's my workaround until TS supports the "exports" resolution scheme

Consuming such module I couldn't figure out how to get intellisense working, nor how to even consume the module from typescript since the types wouldn't load when i import submoduleB from 'module/submoduleB'. "typings" or "types" are not working, they point to a file, not a folder, i can't get TS to compile my module structure into a single .d.ts file. So i enabled --moduleResolution to see what's up and just ended up abusing the typesVersions field to get what i need.

Now my module consumers can

  • import submoduleB from 'module/submoduleB' in both ESM JS and TS
  • const submoduleB = require('module/submoduleB')
  • get intellisense
  • get types
  • ๐ŸŒดshake ๐ŸŽ‰

My published folder structure

module/
โ”œโ”€โ”€ dist/
โ”‚   โ”œโ”€โ”€ browser/
โ”‚   โ”‚   โ”œโ”€โ”€ submoduleA/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ index.js
โ”‚   โ”‚   โ””โ”€โ”€ submoduleB/
โ”‚   โ”‚       โ””โ”€โ”€ index.js
โ”‚   โ”œโ”€โ”€ node/
โ”‚   |    โ”œโ”€โ”€ cjs/
โ”‚   |    โ”‚   โ”œโ”€โ”€ submoduleA/
โ”‚   |    โ”‚   โ”‚   โ””โ”€โ”€ index.js
โ”‚   |    โ”‚   โ””โ”€โ”€ submoduleB/
โ”‚   |    โ”‚       โ””โ”€โ”€ index.js
โ”‚   |    โ””โ”€โ”€ esm/
โ”‚   |        โ”œโ”€โ”€ submoduleA/
โ”‚   |        โ”‚   โ””โ”€โ”€ index.js
โ”‚   |        โ””โ”€โ”€ submoduleB/
โ”‚   |            โ””โ”€โ”€ index.js
|   โ””โ”€โ”€ types/
|       โ”œโ”€โ”€ submoduleA/
|       โ”‚   โ””โ”€โ”€ index.d.ts
|       โ””โ”€โ”€ submoduleB/
|           โ””โ”€โ”€ index.d.ts
โ””โ”€โ”€ package.json

My package.json contents (important ones)

{
  "exports": {
    "./*": {
      "import": "./dist/node/esm/*.js",
      "browser": "./dist/browser/*.js",
      "require": "./dist/node/cjs/*.js"
    }
  },
  "typesVersions": {
    "*": { "*": ["./types/*"] }
  },
  "files": [
    "dist"
  ]
}


Example resolution: (Click to expand)

โฏ npx tsc --traceResolution --skipLibCheck --target ES2020 --module ES2020 --moduleResolution node some.ts

======== Resolving module 'josev2/jwe/compact' from '/Users/panva/repo/esm/some.ts'. ========
Explicitly specified module resolution kind: 'NodeJs'.
Loading module 'josev2/jwe/compact' from 'node_modules' folder, target file type 'TypeScript'.
Found 'package.json' at '/Users/panva/repo/esm/node_modules/josev2/package.json'.
'package.json' has a 'typesVersions' field with version-specific path mappings.
'package.json' has a 'typesVersions' entry '*' that matches compiler version '4.0.3', looking for a pattern to match module name 'jwe/compact'.
Module name 'jwe/compact', matched pattern '*'.
Trying substitution './types/*', candidate module location: './types/jwe/compact'.
File '/Users/panva/repo/esm/node_modules/josev2/types/jwe/compact.d.ts' exist - use it as a name resolution result.
Resolving real path for '/Users/panva/repo/esm/node_modules/josev2/types/jwe/compact.d.ts', result '/Users/panva/repo/esm/node_modules/josev2/types/jwe/compact.d.ts'.
======== Module name 'josev2/jwe/compact' was successfully resolved to '/Users/panva/repo/esm/node_modules/josev2/types/jwe/compact.d.ts' with Package ID 'josev2/types/jwe/[email protected]'. ========

@weswigham Why this feature has not been added to 4.1? Is it possible to add it to 4.2?

Node v14 is going LTS in 10 days, this is the only thing currently stopping me from using the new exports so far, would love to see this implemented.

I think we'll see a spike of people wanting to use this soon, any idea when/if we can expect support for it?

Any updates on this?

This will be great! I can't wait!! Thanks for all the great work on TS so far TS team.

We just encountered this when using exports on a package. The types did not work for <package-name>/test, so we had to manually add paths so that Typescript could find the correct location for the type definitions.

Looking forward to this issue being solved as well ๐Ÿ™

Just a follow up to my previous comment(s) โ€“ this approach will _always_ work:

// package.json
{
  "name": "foobar",
  "types": "index.d.ts", // root/main module types only
  "files": [
    "*.d.ts", // root types
    "sub1", // all `foobar/sub1` files
    "sub2", // all `foobar/sub2` files
    "dist" // all `foobar` files
  ],
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    },
    "./sub1": {
      "import": "./sub1/index.mjs",
      "require": "./sub1/index.js"
    },
    "./sub2": {
      "import": "./sub2/index.mjs",
      "require": "./sub2/index.js"
    },
    "./package.json": "./package.json"
  },
  // ...
}

And then the built file structure:

foobar
โ”œโ”€โ”€ dist
โ”‚ย ย  โ”œโ”€โ”€ index.js
โ”‚ย ย  โ””โ”€โ”€ index.mjs
โ”œโ”€โ”€ sub1
โ”‚ย ย  โ”œโ”€โ”€ index.js
โ”‚ย ย  โ”œโ”€โ”€ index.mjs
โ”‚ย ย  โ””โ”€โ”€ index.d.ts # `foobar/sub1` types only
โ”œโ”€โ”€ sub2
โ”‚ย ย  โ”œโ”€โ”€ index.js
โ”‚ย ย  โ”œโ”€โ”€ index.mjs
โ”‚ย ย  โ””โ”€โ”€ index.d.ts # `foobar/sub2` types only
โ”œโ”€โ”€ index.d.ts     # `foobar` (main) types only
โ””โ”€โ”€ package.json

TypeScript gives index.d.ts the same default-resolver behavior that Node.js gives index.js. This means the sub1/index.d.ts and sub2/index.d.ts is important. So when you import { foo } from 'foobar/sub1' the sub1 directory is accessed and the index.d.ts within it is autoloaded.

The name & location of the main module's definitions (/index.d.ts in tree above) does not matter, since the "types" field points to its location.

Was this page helpful?
0 / 5 - 0 ratings