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.
-
00:06 Create package.json
"name": "<some-package-name>", "license": "MIT"
-
00:12 Create index.ts
-
00:16 Add typescript as a dev dependency
pnpm add -D typescript
-
00:20 Initialize a boilerplate config file for typescript (see Note 1)
pnpm tsc --init
-
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 } }
-
00:31 Initialize git
git init
-
00:35 Create a .gitignore file and add the following lines
node_modules dist
-
00:38 Add tsup as a dev dependency so that typescript files can be bundled into JavaScript files
pnpm add -D tsup
-
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" }
-
00:51 Execute the build
pnpm run build
-
Note that the build creates the following files
- dist/index.js
- dist/index.d.ts
- dist/index.mjs
-
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"
-
01:14 To ensure code quality, add a script for linting
"lint": "tsc"
-
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 -
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
-
01:31 Initialize a configuration file to handle versioning
pnpm changeset init
-
01:33 Note the addition of the following after initialization
- .changeset/config.json – the config file
- .changeset/README.md – information on this dependency
-
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"
-
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
-
01:50 To add a changeset, execute
pnpm changeset
and follow the promptsWhat kind of change? <patch, minor, or major> Summary <a brief description of the change>
-
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
-
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
-
02:19 Setting your CI up like this ensures that each time you make changes, it’s all good
-
02:23 You can view logs at GitHub that show how each step completes successfully
-
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
-
02:53 This published workflow creates a PR that does the following:
- removes the changeset and adds it to a changelog
- versions the package
-
03:05 On merge, the package will be deployed automatically to NPM
-
03:11 So if a contributor includes a changeset in his/her PR, everything is taken care of once you merge
-
03:19
pnpm
really speeds things up because it has a really fast cache -
03:23 Using
tsup
is a great way to serve upcjs
andesm
modules -
03:27 Typescript handles linting
-
03:30 Changesets handle deployment
-
Matt doesn’t mention this but
tsc
is a command line tool made available by installing TypeScript in Step 3. Executingpnpm 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. */ } }
-
"noUncheckedIndexedAccess": true
tightens up a couple of typescript rules -
"noEmit": true
allows for using typescript as a linter -
Although not covered in Matt's tutorial, the documentation states that the files created by the arguments
esm
andmjs
in the build script depend on this setting in package.json"type": "module"
If
type
has not been setdist |-- index.js # cjs `-- index.mjs # esm
If
type
has been setdist |-- index.cjs # cjs `-- index.js # esm
For reference, Matt does not configure this setting in his tutorial.
-
The flag
--dts
generates the declaration file ./dist/index.d.ts (source) -
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
-
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.
-
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.
-
Grant the necessary permsions to publish.yml (see Step 25). See the docs for more information.
permissions: contents: write pull-requests: write
-
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
-
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. -
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. -
Using the command line at the root of your project, execute the following commands.
npm login npm publish