Publish My First NPM TypeScript Package

Minh-Phuc Tran on Dec 24, 2020

javascript

npm

opensource

typescript

While I'm building my website/blog, there are a few things that I see can be useful to separate and publish as 3rd packages - the biggest motivation is to tear down my codebase and avoid maintaining as much irrelevant code as possible.

So, I decided it's a good chance to learn, create, publish my first NPM package ever, and finally share to motivate and help others 🎉. Let's jump into it.

Requirements

It is coming in 2021, I wanted to publish my NPM package in a modern-ish way so that I can re-apply later and my packages stay relevant as long as possible, so I put down some requirements:

  • TypeScript: it has to support TypeScript. Using a package without TypeScript support in 2020 always feels not right for me.

  • Concise documentation.

  • Release workflow: takes less than 1 min. I don't want to completely automate this as I don't think I'll release that many times, automating seems to overkill a quick simple command.

  • Auto-upgrade dependencies: stay up-to-date with all dependencies to avoid security issues, I don't want to take care of this.

  • Prettier code style: standardized, zero configuration.

  • Call to action. It's always good to put a note to the end of what you created and redirect it back to your primary online presence, I believe.

What the package is about?

To build an automatic crosspost to DEV.to (this post you're reading is automatically cross-posted to DEV.to when I pushed it to my website), I need to convert my Markdown posts into a Markdown variant that renders properly on DEV.to. One of these features is that every wrap (virtual newline in a paragraph to make it readable on code editor) is rendered as a newline character on DEV.to, aka, DEV.to unexpectedly breaks a paragraph into multiple paragraphs. To solve it, I wrote a Remark plugin to replace all wraps by spaces.

JavaScript
module.exports = () => (tree) => {
  visit(tree, "text", (text) => {
    text.value = text.value.replace(/\n/g, " ");
  });
};

The code is as simple as that but is quite re-usable, so I decided to make it an NPM package. (It's my first package, it should be simple right?)

I called it remark-unwrap-texts.

Create a TypeScript repo

Initialize a Git repo:

mkdir remark-unwrap-texts
cd remark-unwrap-texts
git init

Create a Github repo for it:

gh repo create phuctm97/remark-unwrap-texts --public

Initialize Yarn/NPM:

yarn init
name: "remark-unwrap-texts"
version: "0.0.0"
author: "Minh-Phuc Tran"
license: "MIT"
private: false

Add TypeScript and Prettier (as dev dependencies):

yarn add -D typescript prettier @tsconfig/recommended

@tsconfig/recommended is a base TypeScript configuration that helps you configure your TypeScript project with minimal code.

Create a tsconfig.json:

{
  "extends": "@tsconfig/recommended/tsconfig.json",
  "compilerOptions": {
    "outDir": "dist",
    "declaration": true
  },
  "include": ["**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

Done ! I got a base TypeScript project.

Write the logic

My package logic requires one library and a type definition package.

  • Install the library:

    yarn add unist-util-visit
    
  • Install the type definition as dev dependencies:

    yarn add -D @types/mdast
    

Write the code, with a little nice documentation:

TypeScript
import { Parent, Text } from "mdast";
import visit from "unist-util-visit";

/**
 * Unwraps `text` nodes in Markdown.
 *
 * Is useful when publishing to platforms like DEV.to, Medium, Hashnode, etc.
 * These platforms may not support text wraps and generate unexpected newlines.
 */
const plugin = () => (tree: Parent) => {
  visit(tree, "text", (text: Text) => {
    text.value = text.value.replace(/\n/g, " ");
  });
};

export = plugin;

Add build information to package.json

Now I got the code, I need to build it into JavaScript as well as a type declaration file. I update my package.json to include these:

{
  // Other attributes.
  "main": "dist/index.js", // for module import/require
  "types": "dist/index.d.ts", // for TypeScript support
  "files": ["dist/**/*"], // includes only build output in the NPM package
  "scripts": {
    "build": "tsc",
    "prepublish": "yarn build", // Make sure output is up-to-date before publishing
    "type:check": "tsc --noEmit"
  }
}

Publish the first version

Publishing with yarn is surprisingly simple:

  • Configure an NPM account to publish to:

    yarn login
    username: "<npm username>"
    email: "<npm email>"
    
  • Publish a new version:

    yarn publish
    New version: "0.0.1"
    password: "<npm password>"
    ... build
    ... publish
    ... Revoked token
    
  • Yarn automatically update package.json with the new version, create a commit and a tag. All you need to do is to push them:

    git push && git push --tags
    

Done ! I got my first NPM package ever published.

Add documentation and tools

  • Create a README:

    • Explain shortly what the package is about.

    • How-to install and use it.

    • Badges from shields.io to show the latest NPM version and the repo's license (also helps add a little character to the repo/package).

    • A Build with 💙 by @phuctm97 at the end.

  • Add a license and code of conduct using Github UI, it helps auto-fill the files for you.

  • Update package.json to update description and keywords displayed on NPM.

    {
      // Other attributes.
      "description": "📋 Unwraps text nodes in Markdown, is useful when publishing to platforms like DEV.to, Medium, Hashnode, etc.",
      "keywords": [
        "markdown",
        "remark",
        "commonmark",
        "unified",
        "remark-plugin",
        "unified-plugin",
        "plugin",
        "extension"
      ]
    }
    
  • yarn publish again to push the updated documentation to NPM.

  • Add .github/dependabot.yml to auto-grade dependencies:

    version: 2
    updates:
      - package-ecosystem: npm
        directory: /
        schedule:
          interval: weekly
    
  • Commit and push ⬆️.

Test and release v1

I've almost done, just gotta test the package in my website implementation to make sure it works:

  • yarn add remark-unwrap-texts.

  • Delete my previous code and replace by require('remark-unwrap-texts').

  • Bump. Everything works correctly!

Go back to remark-unwrap-texts:

  • yarn publish with version 1.0.0.

  • git push && git push --tags.

I got my first NPM package released 🎉!

Hope it helps you publish your first NPM package soon, too. For more details in practice, you can checkout the repository and the NPM package.

Subscribe to the newsletter

Get emails from me about software development, SaaS, and early access to new articles.