Tips for organizing an NPM package containing essential tools

Currently facing the challenge of creating an NPM package to streamline common functionality across multiple frontend projects in our organization. However, I am uncertain about the correct approach. Our projects are built using Typescript, and it seems that tsdx handles many aspects I am unsure about. The real question is how to structure a "utilities" type package.

I am puzzled by what the "main" field in the package.json should point to when there isn't a clear single export in the package. Should every public function be exported individually? How would this impact tree-shaking, which I do not fully grasp yet?

If not exporting everything, then what should "main" point to, and how should imports and exports be handled? Ideally, I want to use something like

import foobar from '@org/common/category/foobar'
, without having extraneous directories like dist or lib in the path.

How can a "multi-function NPM package" be structured for clean imports, efficient tree-shaking, and other optimal practices?

Are there any straightforward examples of such libraries on GitHub or other platforms? I've looked into lodash, but its setup with non-Typescript code and complex configurations like mono-repos and custom build scripts make it challenging to decipher.

Answer №1

What is the best way to structure a multi-function NPM package for clean imports and efficient tree-shaking?

Embracing Modern ES Modules

For modern libraries that need to be compatible with both Node.js and browsers, it is recommended to develop your source code in TypeScript and provide an ES module distribution.

This approach aligns with the idea that new libraries should prioritize modernity, simplicity, and efficiency — essentially, your library should consist of a directory of ES modules accessible publicly.

Your package.json file should contain:

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

You may also configure your tsconfig.json file with entries like these:

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

All concerns related to optimization such as bundling, minification, tree-shaking, transpilation, and polyfilling are left to the consumer applications to handle, as providing for these concerns would result in redundant bloat within the libraries.

Legacy CommonJS applications can easily utilize an ESM adapter to consume the ES module distribution.

If absolutely necessary (although it's highly advisable to reconsider), you can create a dual ESM + CJS hybrid package by running a parallel build step using

tsc -- --module=commonjs --outDir dist-cjs
and then setting the package.json entry as "main": "dist-cjs/main.js", thus supporting both CommonJS and ESM simultaneously.

Specific Recommendations

I'm tasked with creating an NPM package for shared functionality across frontend projects in our organization. However, I'm unsure about the proper way to proceed. We use TypeScript, and while tsdx seems helpful, it doesn't address the structuring aspect for a utility-oriented package.

The ES module strategy I advocate works well for isomorphic code, ensuring seamless integration between frontend and backend node projects.

Regarding the 'main' field in package.json, how should it be handled when there isn't a specific export or function that serves as a logical entry point for the package?

Omit specifying the 'main' and 'module' fields in package.json; it's considered a best practice to avoid designating a single entry point as the "main" — instead, encourage consumers to select modules explicitly.

Utilizing direct imports as illustrated above is preferable to using an index file that consolidates potentially unnecessary modules, eliminating the need for extensive tree-shaking.

Should every public function within the package be exported individually? And if so, how does this impact tree-shaking and other optimizations?

Instruct npm to publish the 'dist' directory containing ES modules from your package.json 'files' array — including 'source' for improved debugging via sourcemaps. Directly importing modules removes tree-shaking constraints since there is no fat index file aggregating possibly unwanted modules.

Often, the packaged path includes 'dist' or 'lib,' which I find visually unappealing. Is there a way to avoid this?

While this aesthetic concern was initially bothersome, embracing the distinction between 'dist' and 'source' contributes to clarity. The simplicity of packages directly from the root enhances user experience.

How can one efficiently structure a multi-function NPM package to achieve clean imports and robust tree-shaking capabilities?

The suggested strategy in this response aims to meet your requirements effectively.

Can anyone recommend simple examples of well-structured libraries on GitHub that demonstrate these principles?

While not an exhaustive list, repositories like authoritarian, renraku, metalshop, and shopper illustrate these practices. Also, established libraries like lit-element serve as good references, despite being partly rooted in the CommonJS realm.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

SailsJS failing to complete updates

It has been a while since I used sails, and I decided to update my old 0.12.14 version to 1.0.2 for a new project I am about to start. I started by attempting $ npm update -g followed by running $ sails -v However, the version returned was still 0.12. ...

What is the syntax for using typeof with anonymous types in TypeScript?

After reading an article, I'm still trying to grasp the concept of using typeof in TypeScript for real-world applications. I understand it's related to anonymous types, but could someone provide a practical example of how it can be used? Appreci ...

Using a modulus operator in Angular 6 to conditionally show elements

Trying to achieve a specific layout for an object in an array using ng-For and ng-If. Here is the desired recovery code, and here's what I've attempted so far in my code snippet enter image description here: <div class="recovery-code" *ngFor= ...

Restore operation initiated through npm was unsuccessful

After running npm run restore following init.cmd in my project, I encountered an issue. Npm seems to be searching for files in the wrong directories. The error message I received reads as follows. Any assistance would be greatly appreciated. Package &ap ...

What npm package is recommended for implementing Google Analytics in an Angular project?

In need of implementing Google Analytics for my Angular 8 project. Can anyone recommend the top npm package to use for adding analytics from the client side? I've come across several packages, but would greatly appreciate feedback from a developer wit ...

NPM's twitter-v2 is throwing a 'TypeError: url_1.URL is not a constructor' error

Currently, my focus is on developing a Twitter bot. I am in the process of posting a Tweet using my account configured to work with Twitter API v2. This involves utilizing NPM's twitter-v2, along with Webpack 5.74.0 and the latest version of NodeJS. ...

Issue encountered in Git: "Unexpected error with option '--short' - Command ended with non-zero exit code 1"

After attempting a fresh installation of a package.json, I encountered the following error: Unfortunately, I am having trouble comprehending the meaning of the error message and searching online for a solution has been unsuccessful so far. ...

What is the best npm package for implementing Fluent UI in Vue.js - @fluentui/web-components or @microsoft/fast-components?

From what I gather, the @fluentui/web-components library has a dependency on @microsoft/fast-components: The web components in the @fluentui/web-components library are created using Microsoft's FAST web component and design system foundation. They u ...

The password encryption method with "bcrypt" gives an undefined result

import bcrypt from 'bcrypt'; export default class Hash { static hashPassword (password: any): string { let hashedPassword: string; bcrypt.hash(password, 10, function(err, hash) { if (err) console.log(err); else { ha ...

Separate the string by commas, excluding any commas that are within quotation marks - javascript

While similar questions have been asked in this forum before, my specific requirement differs slightly. Apologies if this causes any confusion. The string I am working with is as follows - myString = " "123","ABC", "ABC,DEF", "GHI" " My goal is to spli ...

How can the return type of a function that uses implicit return type resolution in Typescript be displayed using "console.log"?

When examining a function, such as the one below, my curiosity drives me to discover its return type for educational purposes. function exampleFunction(x:number){ if(x < 10){ return x; } return null } ...

Find the combined key names in an object where the values can be accessed by index

I am currently working on creating a function called indexByProp, which will only allow the selection of props to index by if they are strings, numbers, or symbols. This particular issue is related to https://github.com/microsoft/TypeScript/issues/33521. ...

I am attempting to retrieve custom cellRendererParams within the CustomCellRenderer class

I'm currently working with Ag-Grid in my angular application and am trying to implement a custom cell renderer. The tutorial I followed uses ICellRendererParams for the parameter type passed to the init event. agInit(params: ICellRendererParams): void ...

Is there a method to prevent explicitly passing the context of "this"?

Currently, I am in the process of developing a new product and have set up both back-end and front-end projects. For the front-end, I opted to use Angular framework with Typescript. As a newcomer to this language (just a few days old), I have encountered a ...

Guidelines for setting the path for prettier in npm

I have been struggling to get Prettier to work properly. Here is the configuration I added in my package.json file: "prettier:write": "prettier --write 'src/*.{ts}'" However, when I run npm run prettier:write, it return ...

Stop committing changes in Git when there are any TypeScript errors found

While working on my project in TypeScript using Visual Code, I encountered a situation where I was able to commit and push my changes to the server through Git (Azure) even though there was an error in my code causing a build failure. It made me wonder i ...

Error: The argument provided cannot be assigned to a parameter that requires a string type, as it is currently a number

Currently, I am in the process of migrating some older websites to TypeScript. However, I keep encountering a type error during the build process. The specific error message is Type error: Argument of type 'number' is not assignable to parameter ...

Integrating Asynchronous NPM with the Meteor Framework

Utilizing meteor-npm to integrate NPM into my Meteor app has been a successful endeavor so far. I have managed to incorporate serialport and xbee-api without any issues. While I am able to read xbee frames using console.log, I am encountering difficulties ...

Issue: Module stream not found in the project directory of C:** ode_modulescipher-baseindex.js. Unable to resolve the module

When I tried running react-native with the npm plugin @woocommerce/api library, I encountered an error in my metro bundler. This plugin is currently active for the WooCommerce REST API. I have attempted various solutions related to the stream and ciph ...

Fill up the table using JSON information and dynamic columns

Below is a snippet of JSON data: { "languageKeys": [{ "id": 1, "project": null, "key": "GENERIC.WELCOME", "languageStrings": [{ "id": 1, "content": "Welcome", "language": { ...