Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for package exports map #1937

Closed
chriskrycho opened this issue May 25, 2022 · 16 comments
Closed

Support for package exports map #1937

chriskrycho opened this issue May 25, 2022 · 16 comments
Labels
enhancement Improved functionality
Milestone

Comments

@chriskrycho
Copy link

chriskrycho commented May 25, 2022

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 chriskrycho added the enhancement Improved functionality label May 25, 2022
@chriskrycho
Copy link
Author

chriskrycho commented May 25, 2022

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

@wenerme
Copy link

wenerme commented Sep 30, 2022

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

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Dec 18, 2022

So... I've spent probably a dozen hours poking at this over the past few months... and it's ridiculously complicated. I ran into microsoft/TypeScript#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

@RichiCoder1
Copy link

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.

@tintinthong
Copy link

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

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Mar 12, 2023

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.

@tintinthong
Copy link

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

@RichiCoder1
Copy link

RichiCoder1 commented Mar 12, 2023

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.

@isaacs
Copy link

isaacs commented Aug 29, 2023

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.

@Gerrit0 Gerrit0 added this to the v0.26.0 milestone Oct 15, 2023
@boneskull
Copy link
Contributor

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.

@isaacs
Copy link

isaacs commented Nov 2, 2023

@boneskull i love this.

@isaacs
Copy link

isaacs commented Nov 2, 2023

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 😅

@jorins
Copy link

jorins commented Dec 14, 2023

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

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Dec 17, 2023

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.

@typhonrt
Copy link

typhonrt commented Jan 13, 2024

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.

Gerrit0 added a commit that referenced this issue Sep 8, 2024
If a package uses a "typedoc" conditional export, then TypeDoc
will use that. If not provided, TypeDoc will use the "import"
and "node" import conditions

Resolves #1937
@Gerrit0 Gerrit0 closed this as completed Sep 8, 2024
@chriskrycho
Copy link
Author

@Gerrit0 thanks for solving this! I know it was not trivial. 🙏🏼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Improved functionality
Projects
None yet
Development

No branches or pull requests

9 participants