6

I need to make an NPM package for some common functionality used by various frontend-projects in our company, but very unsure how to do it properly. We're using Typescript, and tsdx seem to handle several things I'm very unsure how to do properly, but it doesn't say anything about how to structure a "utilities" type package.

What I don't understand is, what should "main" in package.json point to, when there's not a logical single export/class/function that makes sense in a package?

Should it just export every single "public" function in the whole package? And if so, how would that affect tree-shaking (which I currently don't understand super well) and things like that?

And if it should not, what should "main" point to, and how should one export and import things? I would for example like to be able to import foobar from '@org/common/category/foobar, but the way packages are created by npm, it seems the packaged path will often end up including dist or lib or something like that, which I really don't want.

How should one structure a "multi-function NPM package" to get both clean imports and working tree-shaking and other good things?

And does anyone have any good, clean, simple examples of libraries like these on GitHub or other available places? I have tried to look at projects like lodash, but they are often not written Typescript, and often seem to have rather complicated setups with mono-repos, workspaces, custom build scripts, etc...

Svish
  • 152,914
  • 173
  • 462
  • 620

1 Answers1

7

How should one structure a "multi-function NPM package" to get both clean imports and working tree-shaking and other good things?

Publish modern es modules

for modern libraries (both node and browser), you should author your source in typescript, and provide an es-module distribution

this strategy is following the philosophy that new libraries should be modern, lean, and minimalistic -- your library should just be a publicly available directory of es modules

your package.json should include

"type": "module",
"main": "dist/main.js",
"module": "dist/main.js",
"files": [
  "dist",
  "source"
],
"scripts": {
  "prepare": "tsc",
  "watch": "tsc -w",
},
"devDependencies": {
  "typescript": "typescript": "^3.8.3"
},

and perhaps your tsconfig.json with entries like these

"baseUrl": ".",
"rootDir": "source",
"outDir": "dist",
"target": "esnext",
"lib": ["esnext", "dom"],
"module": "esnext",
"experimentalDecorators": true,
"sourceMap": true,
"declaration": true,

all optimizations concerns -- that is: bundling, minification, tree-shaking, transpilation, polyfilling, etc -- all these concerns are already handled by the consumer applications (it's redundant bloat if libraries provide for these concerns)

legacy common-js applications can easily use an esm adapter to consume the es module distribution

if you absolutely must (please consider otherwise), you can make yours a dual esm+cjs hybrid package: just run a parellel build step tsc -- --module=commonjs --outDir dist-cjs and then set package.json "main": "dist-cjs/main.js", and you can support both commonjs and esm simultaneously

Specific answers

I need to make an NPM package for some common functionality used by various frontend-projects in our company, but very unsure how to do it properly. We're using Typescript, and tsdx seem to handle several things I'm very unsure how to do properly, but it doesn't say anything about how to structure a "utilities" type package.

this is why i recommend the esm strategy: it works great for isomorphic code, that is, the library works seamlessly on frontend projects as well as backend node projects

What I don't understand is, what should "main" in package.json point to, when there's not a logical single export/class/function that makes sense in a package?

package.json "main" and "module" fields are optional -- skip it

i think it's a best practice: don't specify a single entry point as your "main" -- instead you should encourage consumers to choose the modules they want explicitly

import {makeLogger} from "authoritarian/dist/toolbox/logger/make-logger.js"

explicit imports like above are better than an index which re-exports a large tree of likely unused modules, it's best to avoid needing to tree-shake at all by only importing what you need

Should it just export every single "public" function in the whole package? And if so, how would that affect tree-shaking (which I currently don't understand super well) and things like that?

you just want to set your package.json "files" array to tell npm to publish the "dist" directory full of es-modules -- and also publish "source" so that consumers will have a good sourcemap experience while debugging anything in the library

encouraging users to directly import modules eliminates the tree-shaking problem -- because you're not using an index file to aggregate potentially unwanted modules, you don't have to shake them out -- it's a consumer's concern anyways, to handle packages which use fat index modules

[...] it seems the packaged path will often end up including dist or lib or something like that, which I really don't want.

yes, this used to aesthetically bother me also, but i got over it

now i like it, packages are simple, from the root, and there is a difference between dist and source (like for accessing sourcemaps for debugging etc) so the distinction is fair game

How should one structure a "multi-function NPM package" to get both clean imports and working tree-shaking and other good things?

i think this answer's strategy will provide what you're looking for

And does anyone have any good, clean, simple examples of libraries like these on GitHub or other available places? I have tried to look at projects like lodash, but they are often not written Typescript, and often seem to have rather complicated setups with mono-repos, workspaces, custom build scripts, etc...

i haven't made popular packages (yet, muahaha!) but my library authoritarian walks the walk.. i've been churning out a lot of libraries lately, and the above principals i'm preaching are what i think i've learned: renraku is my isomorphic json-rpc library, metalshop is my web component library, shopper is my shopify cart ui

other libraries not made by me that are pretty good include lit-element.. most libraries are not new and are half-stuck in the commonjs world

ChaseMoskal
  • 7,151
  • 5
  • 37
  • 50
  • one more note: i think it's a good idea to create a `dist/internal/` area for private functionality, explaining in the readme that anything in the internal directory is fertile grounds for breaking changes and one should stay out – ChaseMoskal May 14 '20 at 21:57
  • How does this work with external dependencies? I tried to set something up with a Yarn workspace that used Express and built into ESM and it gave me an error saying `express is not a function`. – rhlsthrm Jul 01 '21 at 16:23
  • > `import {makeLogger} from "authoritarian/dist/toolbox/logger/make-logger.js"` Please don't encourage this.. It's bad practice and overly "unopinionated" – Christopher Wirt May 27 '22 at 22:46
  • now a couple years later, and i often include a `main` entrypoint.. but it's tricky, because you often have to decide whether your entrypoint is for browsers, node, or both -- it's very easy to include an import in the entrypoint that excludes compatibility -- so lately i've been including a `/dist/node.js` entrypoint for node, or a `dist/browser.js` for browser, and setting `main` to one, or the other, or neither – ChaseMoskal May 29 '22 at 05:18