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

Recommendation for exposing multiple TypeScript modules from single NPM package #8305

Closed
joewood opened this issue Apr 26, 2016 · 40 comments
Closed
Labels
Docs The issue relates to how you learn TypeScript Question An issue which isn't directly actionable in code @types Relates to working with .d.ts files (declaration/definition files) from DefinitelyTyped

Comments

@joewood
Copy link

joewood commented Apr 26, 2016

The module resolution logic in the handbook doesn't mention how nested modules are supported from a single npm package. There is an old issue discussing this that has been closed: #5804

Neither the solution here or simply trying to refer to modules beneath the root dir referred to by the typings or main path seems to work. Is there a documented best practice? The documentation says that ambient modules should not be used in the typings field in package.json.

Just to be clear, I want to write this in a consuming app:

import {stuff} from "my-npm-lib/submodule"

Where the package.json file in my-npm-lib has:

    "main": "lib/index.js",
    "typings": "lib/index.d.ts",

And the file structure in the my-npm-lib:

my-npm-lib\
    index.js
    index.d.ts
    submodule.js
    submodule.d.ts

Currently this complains with a module not found error. But this works, but shouldn't:

import {stuff} from "my-npm-lib/lib/submodule"

This is all with the module resolution setting set to node.

@mhegazy
Copy link
Contributor

mhegazy commented Apr 26, 2016

We need to add documentation, so leaving this issue open to track that, and marking it as such.

For the question, the recommendation is to generate the .d.ts with 1:1 mapping to your .js files, and place them next to the .js file in the published package, as you mentioned. the "typings" field specify the main entry point, matching "main" for the .js file.

I have a sample posted at https://github.com/mhegazy/npm-dependency-test, take a look and let me know if you have more questions.

@mhegazy mhegazy added Question An issue which isn't directly actionable in code Docs The issue relates to how you learn TypeScript @types Relates to working with .d.ts files (declaration/definition files) from DefinitelyTyped labels Apr 26, 2016
@mhegazy mhegazy self-assigned this Apr 26, 2016
@joewood
Copy link
Author

joewood commented Apr 26, 2016

Thanks @mhegazy
As I thought. The only real way of achieving submodules is to pollute the root directory. I think that's a limitation of JavaScript/npm too though, so not a massive deal.

One option to avoid the pollution is to use a postinstall script to only pollute the root dir after npm install. This works fine, but it doesn't work too well with npm link. Another option could be to just move the build files from a distribution dir to the root in postinstall. Anyway, right now I've just resorted to adding a files.exclude filter in vscode to hide the mess.

As you suggested, it would be great to update the handbook on best practices for distributing typescript built libraries, especially with respect to typings and dependent types.

@jcansdale
Copy link

@mhegazy I'm confused about why there is the need for a separate "typings" entry when the .d.ts is invariably next to the one referenced by "main". Why not simply look next to the "main" .js or at least look there as well?

@mhegazy
Copy link
Contributor

mhegazy commented Jun 7, 2016

Not sure i see the issue really. if you are adding "main" property in your package.json, should not be too much to ask to add "typings" as well.

@jcansdale
Copy link

jcansdale commented Jun 7, 2016

@mhegazy It's more that is seems inconsistent. From my understanding, if you: import foo = require("foo.js"), TypeScript tools will automatically look for type definitions in "foo.d.ts". If however you: require("bar") and there is a "main": "foo.js" in bar/package.json, TypeScript tools won't automatically look for type definitions in "bar/foo.d.ts".

This is a shame because otherwise you could convert a JavaScript package into TypeScript by simply renaming the .js files to .ts and compiling with the definitions option. Unfortunately you need to know about the "typings" option otherwise your consumers won't see your type definitions. It just seems like an unnecessary gotcha. Why not support "main" relative as well as "typings"?

Does that make sense?

@mhegazy
Copy link
Contributor

mhegazy commented Jun 9, 2016

it does work for index.js -> index.d.ts. We could add support for main. but the idea was if you chose to change the defaults for your package by setting a main, then you should also consider what you want to put in typings, they may or may not be in the same place.

@jcansdale
Copy link

How about making it so that if typingsexists look for it there, otherwise look for it relative to main? Maybe there is one but I can't think of a good reason why you wouldn't want to look next to main if a typings entry doesn't exist. I'm thinking more for package authors who've written their package using TypeScript, rather than authors who want to add typings to their JavaScript package.

@mhegazy
Copy link
Contributor

mhegazy commented Jun 9, 2016

feel free to file a separate issue for this proposal.

@bogdanmanate
Copy link

This will definitely be a major improvement for Typescript development. I'm currently developing a library in TS and this library should be included in an Angular 2 application. I've also looked into angular repository to see how they are exporting the modules, but it seems that they bundle the modules in a single file.

@mhegazy
Copy link
Contributor

mhegazy commented Jul 22, 2016

here is the documentation for writing declaration files. please let us know if there are any missing scenarios.

@mhegazy mhegazy closed this as completed Jul 22, 2016
@bogdanmanate
Copy link

Hi @mhegazy and thank you for your answer. Where can I find the documentation ?

@mhegazy
Copy link
Contributor

mhegazy commented Jul 22, 2016

Apologies. forgot to add the link. here you go: https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/declaration%20files/Introduction.md

also fixed the original comment.

@pksorensen
Copy link

My usecase is the following, my project builds and output the generated d.ts files to

/dist/typings

and I then in my package.json have specificed that types:"dist/typings/index.d.ts".

From consumer project the following work and it consumes the /dist/typings/index.d.ts file

import * as doh from "libproject"

but when doing

import * as foo from "libproject/test"

it cannot find the /dist/typings/foo/index.d.ts file that was also generated as part of the libproject.

Copying stuff to node_modules/libproject/dist/typings to node_modules/libproject/ as part of install is not really a clean and nice solution.

@bogdanmanate
Copy link

@pksorensen I'm using the same workflow, but I'm also finding it not so elegant and clean. It will be nice to have a method/process that allows you can export a single file (or archive) and than you're good to go. The developers that have already worked with JAVA may relate this to a library export as a .jar file.

@pksorensen
Copy link

I found a better approach. In my consumer project wiht typescript 2.0 I could set it up to resolve submodules.

See the compile time section of readme at https://github.com/s-innovations/kolayout , where setting path compile option of tsc to resolve "mylib/*" to the dist folder of d.ts files of my mylib.

@Meligy
Copy link

Meligy commented Nov 9, 2017

I have the same flow as described in #8305 (comment)

That is, using a subfolder for generated files (.js and .d.ts).

When you do that, and you have:

- index.ts
- sub-module
  - index.ts
- built
  - index.js
  - index.d.ts
  - sub-module
    - index.js
    - index.d.ts

Setting the types and / or typings property in package.json to built/index.js makes this work:

import * as mainLib from 'main-lib';

However, this still fails:

import * as subModule from 'main-lib/sub-module';

(Of course this works import * as subModule from 'main-lib/built/sub-module'; but is undesirable)

If I'm reading above right, this is supposed to work if I'm compiling to the same source directory.

Is there a way to make it work if I compile to a sub-directory?

@mhegazy
Copy link
Contributor

mhegazy commented Nov 9, 2017

Is there a way to make it work if I compile to a sub-directory?

no there is not currently..

We could in theory do multiple lookups if main-lib/sub-module failed to load. first go up to main-lib and resolve sub-module as a module of it, i.e. resolve main-lib first, then use that as a base for finding sub-module. that would be a new feature though, and we would diverge from eh node resolution logic. i would rather we did not do that, so many assumptions are already backed in based on the node resolution logic.

@sweeetland
Copy link

Almost four years later and still no practical solution to this? 🤔

@tomsh99
Copy link

tomsh99 commented Mar 17, 2020

Any solution for this frustrating problem?

@vankessel
Copy link

Ditto ☝️ Would be nice to be able to do this

@jaens
Copy link

jaens commented Mar 21, 2020

This used to be a fundamental restriction of Node.js, but recently there's added support for the exports field in package.json which does allow this.

TypeScript support is tracked in #33079.

emk added a commit to faradayio/vault-env-js that referenced this issue Aug 23, 2020
This package exports multiple modules which are intended to be required
separately. This is a bit tricky with TypeScript:

microsoft/TypeScript#8305
microsoft/TypeScript#33079

The only way to make this work is to get rid of the "./src" and "./dist"
directories and dump everything at the top level. Ick.
@panva
Copy link

panva commented Oct 2, 2020

Any solution for this frustrating problem?

#33079 (comment)

@slavafomin
Copy link

I've also spent a lot of time today trying to figure out why my declaration files are not loaded. I was compiling my declarations to the dist/types/*.d.ts directory. I was expecting TypeScript to be able to resolve type modules relative to the dist/types/index.d.ts file specified in package.json: "types": "dist/types/index.d.ts". However, it wasn't the case. I've tried to use the following option: "types": "dist/types/" to specify the root directory, but it wasn't working either.

I guess it would be very reasonable to expect from TypeScript to look for declarations relative to the directiry/file specified in types property of package.json manifest.

I can't quite comprehend why this issue is closed without a solution…

@Toliak
Copy link

Toliak commented Mar 9, 2021

@slavafomin, the same... I have reproduced this bug (I think it is disagreeable bug...) with rxjs@7.0.0-beta.12.
There is "types" key in package.json
However, auto import suggests nothing for example for pipe (and this variable is exported from index.d.ts)

UPD: Well, I saw, that dev-dependencies are not being indexed on auto-suggesting. Thats the another issue. Maybe installing your package as dependency (not dev-dependency) will help you..

@tstewart-klaudhaus
Copy link

Thanks to @chriskrycho for the very clear description of the situation and workaround in his article linked from #8305 (comment)

The fact that the TS compiler cannot resolve submodule definitions that were generated and positioned within the package structure by the TS compiler itself is pretty remarkable. As a case of the TS compiler not being able to consume its own output, it could be viewed as an internal inconsistency.

My use case is providing a submodule with interfaces intended to be augmented (merged) via ambient declaration. This is itself a workaround for TS lack of higher-kinded types. I get why the HKT thing is difficult, but this submodule resolution issue seems on the surface like it should be much more tractable.

@softmarshmallow
Copy link

softmarshmallow commented Jun 24, 2021

I'm having same problem while using one package for both npm publication and for using as submodule package as my other typescript project.

specifying main should point index.ts file and tsc or yarn/npm should somehow automatically link to dist/index.js

or perhaps this should be another thread fro npm & package.json?

This is a huge pain since we share a lot of project as submodule & for both npm in more thatn 100+ repos

@Stvad
Copy link

Stvad commented Aug 28, 2022

#33079 seems to be fixed, but I still can't find a great working example of a library with submodules hierarchy set up. Anyone has good references?

@ShaMan123
Copy link

I have spent hours trying to configure a repo to work with submodule types. The repo has a build tool, rollup, and a folder structure that doesn't reflect the submodule importing scheme.

#8305 (comment) has pointed out rxjs so I looked at the config they've done since they manage to expose typed submodules with a build tool and w/o mirroring the submodule importing scheme as suggested in the official guide.

Related: #17945

Findings

It seems that though TS doesn't respect the exports.*.types field in package.json it does respect using typesVersions to specify submodule resolution strangely enough (e.g. fabricjs/fabric.js@b326dd6)
I am not sure if this is enough on its own or if you need to define paths in tsconfig.json so I did that as well.
I am sure that defining paths in tsconfig.json w/o the package.json config doesn't work (though I read it should).
Also it seems relative paths are not recognized so use abs syntax that is relative to root.

Hope this helps someone.
TS if it works with typesVersions sounds like it can work with the exports.*.types field in package.json.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Docs The issue relates to how you learn TypeScript Question An issue which isn't directly actionable in code @types Relates to working with .d.ts files (declaration/definition files) from DefinitelyTyped
Projects
None yet
Development

No branches or pull requests