typedoc icon indicating copy to clipboard operation
typedoc copied to clipboard

Support for package `exports` map

Open chriskrycho opened this issue 3 years ago • 15 comments

Search Terms

  • exports
  • TypeScript 4.7
  • packages
  • ES Module

Problem

Today, if you use exports with TS 4.7 to create a public API that is different from the layout you author in, TypeDoc preserves the authoring API rather than the consuming API.

Consider a structure like this:

my-project/
  src/
    public/
      index.ts
      other-module.ts
    -private/
      internal-only.ts

The tsconfig.json specifies an outDir of dist, resulting in this output:

my-project/
  dist/
    public/
      index.js
      index.d.ts
      other-module.js
      other-module.d.ts
    -private/
      internal-only.js
      internal-only.d.ts

Finally, package.json has this:

{
  "exports": {
    ".": "./dist/public/index.js",
    "./package.json": "./package.json",
    "./*": "./dist/public/*.js"
  },
}

This means that Node consumers (and any tools following its resolution algorithm) can now import not only from the index.js but from other-module as well:

import { something } from 'my-project';
import { somethingElse } from 'my-project/other-module';

The docs generated by TypeDoc, however, render the modules without reference to the exports map in package.json, so you end up with a list of modules like this:

  • public
  • public/other-module

For a real-world example of this, see True Myth and its docs.

Suggested Solution

The resolved module map should follow the resolution algorithm from exports in package.json the way TS itself does, so that the resolved module name is based on the mapping defined there.


Note: I know 4.7 came out literally this week, and that this is likely non-trivial! Just figured I'd write it up to get it here so if folks search they will see that it's open.

chriskrycho avatar May 25 '22 16:05 chriskrycho

Possibly a duplicate of #1934, which I somehow missed despite looking at all open issues, searching, etc.: and it was right in front of my face! 🤦🏼‍♂️ Feel free to close/merge with that one as makes sense! (It doesn't overlap 100%, but it may make sense to track in one spot.)

chriskrycho avatar May 25 '22 16:05 chriskrycho

How about support entryPoints in package.json first, I can live without exports support (not easy) as long as the generated doc contain the def.

{
  "typedoc": {
    "entryPoints": ["./src/index.ts","./src/server.ts"],
    "readmeFile": "./README.md"
  }
}

https://github.com/wenerme/wode/blob/6c111c343a1e575cef6795be91cffaa6ad9359ec/packages/utils/package.json#L96-L99

wenerme avatar Sep 30 '22 18:09 wenerme

So... I've spent probably a dozen hours poking at this over the past few months... and it's ridiculously complicated. I ran into https://github.com/microsoft/TypeScript/issues/50466 today.

How about support entryPoints in package.json first, I can live without exports support (not easy) as long as the generated doc contain the def.

PR welcome

Gerrit0 avatar Dec 18 '22 20:12 Gerrit0

I'd be willing to take a peek at implmeneting this! Though it appears there's a pending release (0.24?) that I should probably wait for before doing so?

If this takes the exports approach, it'd likely be easier and/or better to follow the new bundler resolution approach introduced in TS 5.0.

RichiCoder1 avatar Mar 01 '23 19:03 RichiCoder1

What is the status of this issue? Is this coming out in 0.24??

tintinthong avatar Mar 12 '23 05:03 tintinthong

Not planned for 0.24. I'm not entirely convinced there's a good way of doing this that'll work for more than 50% of packages, at least without requiring a custom resolution mode to tell TD what to use. The problem is mainly projects which ship esm and cjs, and use the exports map to associate different build outputs which each.

Gerrit0 avatar Mar 12 '23 06:03 Gerrit0

Not planned for 0.24. I'm not entirely convinced there's a good way of doing this that'll work for more than 50% of packages, at least without requiring a custom resolution mode to tell TD what to use. The problem is mainly projects which ship esm and cjs, and use the exports map to associate different build outputs which each.

Perhaps then its worth pursuing this pr suggestion. I will work on it if there is no current work ongoing

tintinthong avatar Mar 12 '23 07:03 tintinthong

without requiring a custom resolution mode to tell TD what to use. The problem is mainly projects which ship esm and cjs, and use the exports map to associate different build outputs which each.

The boring answer might just be to always assume import condition (since a lot of commonjs still use ESM and then downcompile), and then plug it into something like https://github.com/lukeed/resolve.exports to keep the complicated exports logic external. Or no condition and let the cards fall where they may, with maybe a config option to specify the default.

RichiCoder1 avatar Mar 12 '23 17:03 RichiCoder1

https://isaacs.github.io/resolve-import/functions/index.resolveAllExports.html will resolve all the valid exports for a given package.json file, returning the file:// URLs that they map to. From there, could either use the source map or reverse-engineer the include and rootDir from tsconfig.

without requiring a custom resolution mode to tell TD what to use. The problem is mainly projects which ship esm and cjs, and use the exports map to associate different build outputs which each.

I'd recommend either just hard-coding the ['import', 'node', 'default'] conditions like @RichiCoder1 suggests, or make it configurable.

Any module that's exporting multiple versions of a single module is going to have something that resolves with ['import', 'node', 'default'], and if it's configurable and they want the 'require' condition to be documented instead, they can specify it.

Another approach would be to document both of the exports for a given export module name, and have one at /modules/exportname_import.html and the other at /modules/exportname_require.html or some such, but that's going to be much more complicated to do and probably not what anyone really wants anyway, since the vast majority of the documentation would be duplicated in any real project.

isaacs avatar Aug 29 '23 20:08 isaacs

Just throwing another idea on the pile: support a specific conditional export. e.g.:

{
  "name": "my-pkg",
  "version": "1.0.0",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "typedoc": "./src/index.ts",
      "default": "./dist/index.js"
    },
    "./something": {
      "types": "./dist/something.d.ts",
      "typedoc": "./src/something.ts",
      "default": "./dist/something.js"
    }
  }
}

Grab the entry points that way.

boneskull avatar Nov 02 '23 04:11 boneskull

@boneskull i love this.

isaacs avatar Nov 02 '23 04:11 isaacs

Maybe it would be worthwhile to propose a "source" or "origin" import condition which conventionally maps back to the source of a module? Seems like something that many other tools might benefit from. Of course, you can always get this from the sourcemap as well, so it might just be a tool that is only useful for lies 😅

isaacs avatar Nov 02 '23 17:11 isaacs

I'm working on a library that has multiple modules per the exports field. A workaround for the time being is exporting submodules from the root module. But I can imagine that would not be desirable in all cases. My library has an index module that only has the lines:

export * as types from './types'
export * as filter from './filters'

and typedoc outputs this:

image

jorins avatar Dec 14 '23 00:12 jorins

The exports support is really only useful for automatically creating modules with the names people will use to import your module with the current version of TypeDoc, it's still something I want to do to make running basically zero config, but it isn't a high priority right now, since module renaming can be done with @module

TypeDoc supports multiple entry points, so you could achieve the same output (with Namespaces changed to Modules) by passing both of those files to TypeDoc.

Gerrit0 avatar Dec 17 '23 23:12 Gerrit0

I have just released a comprehensive solution for this issue: @typhonjs-typedoc/typedoc-pkg. I have created a zero configuration CLI front end for TypeDoc that analyzes package.json to automatically generate docs for well configured packages. It fully supports exports including sub-path patterns. There also is mono-repo support where it is easy to target and generate a complete set of docs for all packages in the mono-repo. By default, the "types" export condition is evaluated with fallbacks to types / typings properties of package.json. However, you can also specify an alternate export condition to generate documentation for and this can be source code exports including Typescript. typedoc-pkg also doesn't require any typedoc.json or tsconfig.json.

typedoc-pkg uses the Default Modern Theme / DMT which brings a bit of polish to the default theme. The package export to module / entry point renaming is facilitated via the DMT, so until TypeDoc provides some sort of configuration option to remap entry point / module names using alternate themes doesn't give you all of the features though you can use any theme you want. There also is easy hookup for complete end-to-end API linking for the modern web supporting the entire Typescript built-in libraries for ES2023 / DOM / Web Worker APIs + a few other leading edge ones like Web Codecs / WebXR.

I'd really like to get some stress tests done with a variety of packages, so please give it a go. I'm using typedoc-pkg for all of my Node packages and updating docs everywhere. Peer dependencies are Typescript 5.1+ and TypeDoc 0.25+. You don't need to install TypeDoc as a dependency (make sure you don't have a .npmrc file in the root of the project w/ legacy-peer-deps set to true).

I spent the last couple of months working on all of this full time, so it's ready to go. I'd like to collect feedback and I'll be working on more documentation and a wiki in the coming weeks then bump things to 0.1.0.


The following is docs hosted on Github for @typhonjs-build-test/esm-d-ts:

https://typhonjs-node-build-test.github.io/esm-d-ts/


And here are the docs and example of using an export condition (typedoc) targeting Typescript entry points for @typhonjs-svelte/trie-search:

https://typhonjs-svelte.github.io/trie-search/

In particular you can see the export conditions and the NPM script for generating from TS source.

typhonrt avatar Jan 13 '24 02:01 typhonrt