thonbecker-first-npm-package v2.0.0
Publish First NPM Package
What is NPM?
Node Package Manager, or NPM, is the world's largest software registry. When we say NPM, we could be referring to a few things:
- The Website - Helpful for discovering JS packages and managing profiles
- The CLI - How most engineers interact with npm
- The registry - Where the JS packages are stored
Every time an yarn install
is run from the CLI, NPM looks at its registry to install all the packages in yarn.lock
.
The purpose of this workshop is to make a package that anyone else can download using npm!
In the real world, when should I make a shared package?
Here are some great considerations on when to make a shared package. When you read these, think of something your team is creating and if it's time to make it a shared package.
Disclaimer: This mostly came from from ChatGPT
- Reusability: Determine if the functionality or code you intend to package is generic and can be reused across multiple projects. Shared packages are beneficial when there is potential for widespread reuse rather than being specific to a single application.
- Modularity: A shared package should ideally be self-contained, independent, and decoupled from the rest of the application so that it can be easily integrated into other systems.
- Collaborative Development: Determine if multiple teams or developers could potentially benefit from the shared package. If the code is valuable to other colleagues or projects within your organization, creating a shared package promotes collaboration and code reuse.
- Versioning and Compatibility: Evaluate the potential for different versions and compatibility concerns. If the code is likely to evolve over time, having a shared package with version management facilitates backward compatibility, easier upgrades, and dependency management.
- Complexity and Size: If the code is relatively small and simple, it might be more efficient to directly include it in your project rather than creating a separate package. However, larger and more intricate components often benefit from being packaged separately.
Creating the Project
Initialization
First, navigate to the test project
cd ./workshops/publish-first-npm-package
mkdir test-project
cd ./test-project
Now initialize the project by running the init
command with these values
yarn init
You can accept the defaults except for this these values:
Attribute | Value |
---|---|
description | My first open source project! |
test command | jest |
author | Your Name |
Adding Typescript
yarn add --dev typescript
And add a tsconfig.json
with these options
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"declaration": true,
"outDir": "./lib",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
Not sure what one of these means? Just look it up, we won't have time in this workshop to cover them.
Next, open package.json
and add/update the scripts
command to this:
"scripts": {
"build": "rm -rf lib && tsc",
"test": "jest"
}
Now try to compile your code! Wait! It should fail because there is no code.
yarn run build
Let's fix that.
Adding Code
Thought we were going to add some code? WRONG!
If you came to the Test Driven Development workshop, then you would know that we are actually going to start with adding the tests!
Let's get jest
installed
yarn add --dev jest @types/jest ts-jest
Let's breakdown what each of these do:
jest
is the testing framework that we will use to run the tests.@types/jest
is needed to provide the types (.d.ts
) files since we are using TypeScriptts-jest
is required for Jest to understand TypeScript (we could've also usedbabel
)
Now it's time to create a Jest configuration file
./node_modules/.bin/jest --init
Accept the defaults by pressing ENTER
for each prompt.
This will create a jest.config.js
file for us with a lot of commented out options.
Open jest.config.js
and change preset
to ts-jest
like so:
{
// ...
preset: "ts-jest",
// ...
}
Finally, we can add the test code by creating the src
directory, adding src/test-project.test.ts
, and pasting this code:
import { add } from '.';
test('adds two numbers together', () => {
expect(add(1, 2)).toEqual(3);
});
Now try running yarn test
and obviously it's going to fail.
Finally, it's time to add the code.
Create a new file src/index.ts
and paste this into it:
function add(num1: number, num2: number) {
return num1 + num2;
}
export { add };
It doesn't really matter what the code is doing, it's just important that the tests are now passing!
Testing the Commands
Now that we have code running, let's compile the TypeScript:
yarn run build
That is going to run tsc
under the hood and compile all TypeScript files into .js
files with their corresponding .d.ts
files.
Open the /lib
file and notice that it also compiled the test file. Should the tests be shipped with the code? No, because that will only bloat the bundle size.
Let's exclude the tests from compilation by opening the tsconfig.json
and adding the exclude
statement to the top
{
"exclude": ["src/**/*.test.ts", "./lib/**/*"],
"compilerOptions": {
// ...
}
}
NOTE: Since this overwrites the default exclude
, the output directory /lib
needs to also be included so tsc
doesn't throw a 'this file is about to be overwritten' error.
Now, rerun yarn run build
and verify that it only outputs index.js
and index.d.ts
.
Linting
Every good project that should be published is linted properly!
The purpose here is just to get the barebones setup, feel free to add more.
yarn add --dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
Add an .eslintrc
file with this config:
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"env": {
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
]
}
Now add a .eslintignore
because ESLint doesn't need to lint everything.
node_modules
lib
Now, add the lint
command to package.json
{
"scripts": {
"build": "rm -rf lib && tsc",
"lint": "eslint .",
"test": "jest"
}
}
Now verify everything works by running
yarn run lint
Formatting
Let's get prettier setup so that our syntax is pretty and consistent!
yarn add --dev prettier
Create a new file called .prettierrc
and copy this into it
{
"printWidth": 120,
"trailingComma": "all",
"singleQuote": true
}
Now open package.json
and add the format
command to the scripts
section like this
"scripts": {
"build": "tsc",
"format": "prettier --write \"src/**/*.ts\"",
"lint": "eslint .",
"test": "jest"
},
Setting up the Publish Commands
Congrats! A lot of the best practices are setup with the package and now we just need to configure the scripts to run everything before publishing.
First, let's check out the npm publish
command.
The publish command is responsible for packaging your files together and publishing them to the NPM repository which will make it available for other people to consume.
Under the hood, it is actually running several other npm commands in this order:
prepublishOnly
prepack
prepare
postpack
publish
postpublish
There's a lot of commands here but only prepublishOnly
and prepare
are going to be used for this example.
Here's what NPM has to say about prepublishOnly
:
If you need to perform operations on your package before it is used, in a way that is not dependent on the operating system or architecture of the target system, use a prepublish script.
Prepublish script includes tasks such as:
- Compiling CoffeeScript source code into JavaScript.
- Creating minified versions of JavaScript source code.
- Fetching remote resources that your package will use.
We're going to use it to format, lint, and test the code.
The prepare
script is the ideal place to build the code.
So, let's add some of these to the scripts
section in the package.json
:
"scripts": {
// ...
"prepublishOnly": "yarn format && yarn lint && yarn test",
"prepare": "yarn build",
//..
}
The full scripts
section should look like this:
"scripts": {
"build": "tsc",
"format": "prettier --write \"src/**/*.ts\"",
"lint": "eslint .",
"prepublishOnly": "yarn format && yarn lint && yarn test",
"prepare": "yarn build",
"test": "jest"
}
Getting Ready to Publish
There's a few things we have to tidy up in the package.json
before publishing.
- Set
main
to"lib/index.js"
because that is wheretsc
will output the main built.js
file. - Add
"types": "lib/index.d.ts"
right belowmain
so TypeScript users know where to find the base type declaration file.
All of the files that have been created so far are going to be bundled by yarn publish
.
That will include the linting and formatting configuration files like these:
.eslintignore
.eslintrc
.prettierrc
jest.config.js
src/index.ts
src/test-project.test.ts
tsconfig.json
These need to be excluded somehow.
Instead of having an excluded list of files (which would have to be updated when a new tool is onboarded), add the files
command in the package.json
to only include files that are needed.
{
// ...
"files": ["lib", "LICENSE", "README.md", "package.json"],
// ...
}
NOTE: The LICENSE
and README.md
will be created later.
Excluding the configuration and test files will make the bundle size much smaller!
Publishing... Finally 🚀!
Having the right name for your project is a very important consideration before publishing. NPM has two options for naming: unscoped and scoped.
You are already familiar with unscoped projects.
These are going to be projects like lodash
, react
, typescript
, etc.
NPM says that unscoped project names must follow these guidelines:
- The name must be unique
- The name cannot be spelled similarly to another package
- The name will not confuse others about authorship
Today, it can be hard to think of a name that isn't already taken. If you do come up with a name, it's often going to be spelled very similarly to another package.
An example would be trying to upload a new package that can recognize music chords.
An obvious name for the package would be chordjs
, but too bad because that's already taken.
So, you try chord-js
which isn't taken.
NPM will reject this name because it's too similar to chordjs
.
The alternative is a scoped project which will normally fall under your GitHub username.
Following the chord example, it would be @patrady/chord-js
.
These projects are a lot easier to name because they're unique per scope.
There's lots of common packages that do this @testing-library/react
, @types/jest
, @typescript-eslint/parser
, etc.
So since test-project
was already claimed 7 years ago, the only option is to either name it something unique or to scope it to your username.
Let's scope it to your username by opening package.json
and changing the name to your GitHub username followed by /test-project
.
{
"name": "@patrady/test-project",
// ...
}
Now, it's time to create your NPM account if you haven't already.
NOTE: Your username should be what you scope /test-project
under.
Then login through your terminal using
yarn login
Now, let's get this thing published 🔥🔥🔥!
yarn publish --access public
NOTE: --access public
is required because scoped packages are published privately by default (and you have to pay for that).
After it publishes, go to NPM's Website and search for your project name.
CONGRATS! You have now contributed your first package to the open source community! Time to add that to your resume!
Importance of a Good README
When you visit your site on NPM, one of the first things you'll notice is that it is very bare.
It also has this text at the top that says
This package does not have a README. Add a README to your package so that users know how to get started.
A README
is incredibly important if you plan on having others discover, install, use, or contribute to your project.
Here's what NPM has to say about a README
To help others find your packages on npm and have a good experience using your code in their projects, we recommend including a README file in your package directory. Your README file may include directions for installing, configuring, and using the code in your package, as well as any other information a user may find helpful. The README file will be shown on the package page.
A large portion of shared projects (even internally at Ramsey) will go unused if an engineer doesn't know how to use the code. That means the code that you spent so many tireless hours on will go unused and replicated simply because you didn't put the time in to explain how to use it.
Spend the extra few hours writing a good README
!
So, let's make a README.md
file and put this in there (at the least):
# Test Project
Are you tired of doing mental math?
This test project will add two numbers together for you!
## Getting Started
### Installation
\```bash
yarn install @patrady/test-project
\```
### Usage
\```ts
import { add } from "@patrady/test-project";
const total = add(1, 2);
console.log(total); // 3
\```
NOTE: Make sure to remove the forward slashes for the code blocks.
NOTE: Make sure to change @patrady
to your scope.
Adding a Badge
While we're at it, let's include a badge at the top of our README
!
Use Shields.io to create one quickly using this format
![Shields.io NPM Badge](https://img.shields.io/npm/v/:scope/:packageName)
An example would be ![Shields.io NPM Badge](https://img.shields.io/npm/v/@patrady/test-project)
Semantic Versioning
Now that the README
is done, the project needs to be published again.
yarn publish
NOTE: --access public
is not needed again because it has been previously published
NOTE: The README
will be added because it is listed in the "files"
array in the package.json
Since the current version of your project 1.0.0
is already taken, NPM will prompt for another version.
Versioning is a huge part of creating open source software and can make or break your project's reputation. For example, some projects are great at this like react
and some projects are notorious for including breaking changes in minor version bumps like Rails.
The versioning standard is semantic versioning, or semver for short.
Here's what Semver 2.0.0 says:
Given a version number MAJOR.MINOR.PATCH, increment the:
1. MAJOR version when you make incompatible API changes
2. MINOR version when you add functionality in a backward compatible manner
3. PATCH version when you make backward compatible bug fixes
Here's some examples for each: 1. MAJOR - Changing the arguments of a public function which are no longer compatible with the previous version - Changing an API's route - Removing public methods, classes, or functionality 2. MINOR - Adding a new public method or API route - Adding a new optional parameter to a public function 3. PATCH - Bug fix - Documentation updates - Security fixes
Since this is adding documentation, the new version should be 1.0.1
.
Type that into the terminal and after it finishes publishing, checkout your project on NPM's website to see the updated screen!
Next Steps
Congrats! You are now an open source contributor!
To finish the workshop, write a new public method called subtract
, add tests, and then publish the new version to npm!
11 months ago