Skip to content

Instantly share code, notes, and snippets.

@samuraijane
Last active April 5, 2024 22:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samuraijane/94debb1a7271390b28e606a6549df4d6 to your computer and use it in GitHub Desktop.
Save samuraijane/94debb1a7271390b28e606a6549df4d6 to your computer and use it in GitHub Desktop.
Publishing to NPM

Blazingly Fast Tips: Publishing to NPM by Matt Pocock

See Note 6 below for changes that have happened since Matt's video was published and Note 7 for a sample repo I created while following his tutorial. The information here is current as of April 5, 2024.

Create a library on your local

  1. 00:06 Create package.json

    "name": "<some-package-name>",
    "license": "MIT"
  2. 00:12 Create index.ts

  3. 00:16 Add typescript as a dev dependency

    pnpm add -D typescript
  4. 00:20 Initialize a boilerplate config file for typescript (see Note 1)

    pnpm tsc --init
  5. 00:22 Update tsconfig.json to the following (see Notes 2 and 3)

    {
      "compilerOptions": {
        "target": "es2016",
        "module": "commonjs",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true,
        "noUncheckedIndexedAccess": true,
        "noEmit": true
      }
    }
  6. 00:31 Initialize git

    git init
  7. 00:35 Create a .gitignore file and add the following lines

    node_modules
    dist
  8. 00:38 Add tsup as a dev dependency so that typescript files can be bundled into JavaScript files

    pnpm add -D tsup
  9. 00:44 Add a build script that bundles typescript as cjs and esm modules (see Notes 4 and 5)

    "scripts": {
      "build": "tsup index.ts --format cjs,esm --dts"
    }
  10. 00:51 Execute the build

    pnpm run build
  11. Note that the build creates the following files

    • dist/index.js
    • dist/index.d.ts
    • dist/index.mjs
  12. 01:02 Update package.json with these three properties listed beneath the line that defines the license

    "main": "./dist/index.js",
    "module": "./dist/index.mjs",
    "types": "./dist/index.d.ts"
  13. 01:14 To ensure code quality, add a script for linting

    "lint": "tsc"
  14. 01:18 Note that executing pnpm run lint will highlight any type errors in the application so that these can be fixed before shipping to production

  15. 01:25 Add a dev dependency to handle the versioning for your package so you don’t have to do it manually

    pnpm add -D @changesets/cli
  16. 01:31 Initialize a configuration file to handle versioning

    pnpm changeset init
  17. 01:33 Note the addition of the following after initialization

    1. .changeset/config.json – the config file
    2. .changeset/README.md – information on this dependency
  18. 01:40 Add an initial version to your package by updating package.json just below the line that defines the license

    "version": "0.0.1"
  19. 01:43 Keep in mind that every time you make a change to your package, you should record it by adding a changeset, which is a markdown file that describes the changes you are making

  20. 01:50 To add a changeset, execute pnpm changeset and follow the prompts

    What kind of change? <patch, minor, or major>
    Summary <a brief description of the change>
  21. 01:54 After completing the prompts, a markdown file with a random name will be added to the .changeset/ directory that has the description you provided earlier

  22. 01:59 Configure continuous integration

    # **.github/workflows/main.yml**
    
    name: CI
    on:
      push:
        branches:
          - "**"  # run on all branches
    
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3  # checks out the repository
          - uses: pnpm/action-setup@v2  # sets up pnpm
            with:
              version: 7
          - uses: actions/setup-node@v3  # sets up node
            with:
              node-version: 16.x
              cache: "pnpm"
    
          - run: pnpm install --frozen-lockfile  # install with a frozen lockfile
          - run: pnpm run lint && pnpm run build  # then run lint and build
  23. 02:19 Setting your CI up like this ensures that each time you make changes, it’s all good

  24. 02:23 You can view logs at GitHub that show how each step completes successfully

  25. 02:29 Add a second workflow file that handles publishing your package

    # **.github/workflows/publish.yml**
    
    name: Publish
    on:
      push:
        branches:
          - "main"  # publish only on the main branch
    
    concurrency: ${{ github.workflow }}-${{ github.ref }}  # ensures that the two publish workflows cannot be happening at the same time
    
    jobs:
      build:
        runs-on: ubuntu-latest
        permissions: # see Note 8
            contents: write
            pull-requests: write
        steps:
          - uses: actions/checkout@v3  # checks out the repository
          - uses: pnpm/action-setup@v2  # sets up pnpm
            with:
              version: 7
          - uses: actions/setup-node@v3  # sets up node
            with:
              node-version: 16.x
              cache: "pnpm"
    
          - run: pnpm install --frozen-lockfile  # install with a frozen lockfile
          - name: Create Release Pull Request or Publish  # this is our last step
            id: changesets
            uses: changesets/action@v1  # uses the changesets action
            with:
              publish: pnpm run build  # then runs the build
            env:
              GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # See Note 8
              NPM_TOKEN: ${{ secrets.<SOME_NPM_TOKEN> }} # See Note 8
  26. 02:53 This published workflow creates a PR that does the following:

    1. removes the changeset and adds it to a changelog
    2. versions the package
  27. 03:05 On merge, the package will be deployed automatically to NPM

  28. 03:11 So if a contributor includes a changeset in his/her PR, everything is taken care of once you merge

  29. 03:19 pnpm really speeds things up because it has a really fast cache

  30. 03:23 Using tsup is a great way to serve up cjs and esm modules

  31. 03:27 Typescript handles linting

  32. 03:30 Changesets handle deployment


Notes to Matt's tutorial

  1. Matt doesn’t mention this but tsc is a command line tool made available by installing TypeScript in Step 3. Executing pnpm tsc --init creates a file that details the available settings along with a brief description of what each one does (see below).

    {
      "compilerOptions": {
        /* Visit https://aka.ms/tsconfig to read more about this file */
    
        /* Projects */
        // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
        // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
        // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
        // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
        // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
        // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
    
        /* Language and Environment */
        "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
        // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
        // "jsx": "preserve",                                /* Specify what JSX code is generated. */
        // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
        // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
        // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
        // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
        // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
        // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
        // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
        // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
        // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
    
        /* Modules */
        "module": "commonjs",                                /* Specify what module code is generated. */
        // "rootDir": "./",                                  /* Specify the root folder within your source files. */
        // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
        // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
        // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
        // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
        // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
        // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
        // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
        // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
        // "resolveJsonModule": true,                        /* Enable importing .json files. */
        // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
    
        /* JavaScript Support */
        // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
        // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
        // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
    
        /* Emit */
        // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
        // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
        // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
        // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
        // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
        // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
        // "removeComments": true,                           /* Disable emitting comments. */
        // "noEmit": true,                                   /* Disable emitting files from a compilation. */
        // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
        // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
        // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
        // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
        // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
        // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
        // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
        // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
        // "newLine": "crlf",                                /* Set the newline character for emitting files. */
        // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
        // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
        // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
        // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
        // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
        // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
    
        /* Interop Constraints */
        // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
        // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
        "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
        // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
        "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
    
        /* Type Checking */
        "strict": true,                                      /* Enable all strict type-checking options. */
        // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
        // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
        // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
        // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
        // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
        // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
        // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
        // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
        // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
        // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
        // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
        // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
        // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
        // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
        // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
        // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
        // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
        // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
    
        /* Completeness */
        // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
        "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
      }
    }
    
  2. "noUncheckedIndexedAccess": true tightens up a couple of typescript rules

  3. "noEmit": true allows for using typescript as a linter

  4. Although not covered in Matt's tutorial, the documentation states that the files created by the arguments esm and mjs in the build script depend on this setting in package.json

    "type": "module"

    If type has not been set

    dist
    |-- index.js    # cjs
    `-- index.mjs   # esm
    

    If type has been set

    dist
    |-- index.cjs    # cjs
    `-- index.js     # esm
    

    For reference, Matt does not configure this setting in his tutorial.

  5. The flag --dts generates the declaration file ./dist/index.d.ts (source)

  6. On September 22, 2023, GitHub announced they are in the process of deprecating GitHub Actions for Node 16. This means that you need to update the following instances:.

    actions/checkout@v3     -->   actions/checkout@v4
    actions/setup-node@v3   -->   actions/setup-node@v4
    npm/action-setup@v2     -->   npm/action-setup@v3
    
  7. I created @samuraijane/pluralit while following Matt's video. It is not intended to be used in production but you may find referencing it helpful when building your own library.

  8. Matt's video covers everything you need to do prior to publishing to NPM but does not go over the steps that actually push your code to the NPM's registry. To do that, see Publish your code to NPM below.


Publish your code to NPM

  1. Grant the necessary permsions to publish.yml (see Step 25). See the docs for more information.

    permissions:
      contents: write
      pull-requests: write
  2. Allow publish.yml to initiate HTTP requests to both GitHub and NPM (see Step 25).

    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # See 3 below
      NPM_TOKEN: ${{ secrets.<SOME_NPM_TOKEN> }} # See 4 below
  3. The publish workflow needs the value of GITHUB_TOKEN in order to create a pull request. GitHub provides this automatically – it is created when the workflow is invoked and expires in 24 hours. For more information, see Automatic token authentication.

  4. In order for the pull request created by the changeset (see Step 20) to publish to NPM on merge, you must authorize the workflow to push to NPM. This is done with NPM_TOKEN: ${{ secrets.<SOME_NPM_TOKEN> }} where <SOME_NPM_TOKEN> is the name of the token you define in your account at NPM. Once you are logged in, click on your avatar, then select Access Tokens. Click Generate New Token, followed by Granular Access Token. Fill in the required fields. The publish workflow only needs the name of the token, not the value.

  5. Using the command line at the root of your project, execute the following commands.

    npm login
    npm publish
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment