Npm
Creating a new Npm Package
Last updated •
Creating and publishing a new Npm package can be a rewarding experience. This is how I typically go about it using TypeScript and Vitest for testing.
1. Initialize your project
mkdir my-new-package cd my-new-package npm init
.npmignore
.github ./src/** tsconfig.json
2. Setup Git
git init
.gitignore
# Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* .vscode # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage coverage-vitest/ coverage-final/ *.lcov # Test reports monocart-report/ playwright-report/ test-results/ # React Router generated files .react-router/ # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # vitepress build output **/.vitepress/dist # vitepress cache directory **/.vitepress/cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* dev.debug .react-router params.bicepparam build/ app.zip # Playwright /test-results/ .v8-coverage/ test-reports/ monocart-report/ coverage-vitest/ coverage-playwright/ coverage-reports/
Follow the directions on screen to set up your package.json file. I typically set the version to 1.0.0-beta.1 for the initial release.
3. Set up TypeScript
pnpm i --save-dev typescript @types/node
tsconfig.json
{ "include": ["src/**/*.ts"], "exclude": ["node_modules/**", "**/*.test.ts"], "compilerOptions": { "strict": true, "target": "ES2022", "module": "ES2022", "moduleResolution": "bundler", "composite": true, "jsx": "react-jsx", "strictNullChecks": true, "skipLibCheck": true, "allowSyntheticDefaultImports": true, "outDir": "dist", "types": ["node"], "declaration": true, "esModuleInterop": true } }
Update your package.json to include the following fields:
{ // define an es module "type": "module", "scripts": { "build": "tsc", "test": "vitest", }, // set the type definitions and main entry point if you have one "typings": "dist/src/index.d.ts", // set the main entry point if ou have one "module": "./dist/src/index.js", // define exports for different environments "exports": { // default export when you import the package ".": { "types": "./dist/src/index.d.ts", "default": "./dist/src/index.js", "import": "./dist/src/index.js", }, "./another-file": { "types": "./dist/src/another-file.d.ts", "default": "./dist/src/another-file.js", "import": "./dist/src/another-file.js", }, "./package.json": "./package.json", }, "files": ["dist/"], }
4. Setup ESLint
pnpm create @eslint/config@latest
Follow the instructions to set up ESLint for a TypeScript project.
5. Create your source files
mkdir src touch src/index.ts echo 'export const hello = () => "Hello, world!";' > src/index.ts
6. Setup Vitest
pnpm i --save-dev vitest
vitest.config.ts
import { defineConfig } from 'vitest/config' export default defineConfig({ test: { environment: 'node', coverage: { reporter: ['text', 'json', 'html'], thresholds: { 100: true, }, }, }, })
mkdir src/tests touch src/tests/index.test.ts
src/index.test.ts
import { describe, it, expect } from 'vitest' import { hello } from './index' describe('hello function', () => { it('should return "Hello, world!"', () => { expect(hello()).toBe('Hello, world!') }) })
7. Setup GitHub Actions for CI
.github/workflows/publish.yml
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages name: Publish Package on: push: branches: [main] # Using OIDC to avoid secrets/tokens for npm publishing # https://docs.npmjs.com/trusted-publishers#step-2-configure-your-cicd-workflow permissions: id-token: write # Required for OIDC contents: read jobs: publish-npm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: pnpm/action-setup@v4 with: version: 10 - uses: actions/setup-node@v6 with: node-version: '24' cache: 'pnpm' - run: pnpm install - run: pnpm test --coverage - run: pnpm run build - run: pnpm publish --tag=beta