DEV Community

Yu-Chen, Lin
Yu-Chen, Lin

Posted on

Stop Reinventing the Wheel: forge-npm-pkg CLI Tool for Best-Practice npm Package Development

Introduction

Every time you create a new npm package, do you find yourself doing this?

// Copy-pasting the same config again...
const tsconfig = {
  compilerOptions: {
    target: "ES2022",
    module: "NodeNext",
    strict: true,
    // Grabbed this from my last project, but is it still best practice?
  }
};
Enter fullscreen mode Exit fullscreen mode
  • package.json exports configuration is way too complex
  • ESM? CommonJS? Dual? Which one should I use?
  • Writing GitHub Actions CI/CD from scratch every time
  • Setting up Dependabot manually again
  • That template from 6 months ago is already outdated...

To solve this "reinventing the wheel every time" problem, I created forge-npm-pkg - a CLI tool that generates npm packages with always up-to-date best practices.

npx forge-npm-pkg my-awesome-package
Enter fullscreen mode Exit fullscreen mode

One command gives you an npm package based on the latest best practices.

πŸ“¦ NPM: https://www.npmjs.com/package/forge-npm-pkg

5 Problems forge-npm-pkg Solves

Problem Traditional Approach forge-npm-pkg Solution
Dependency versions Hardcoded in templates β†’ outdated in 6 months Dynamically fetched from npm registry
Node.js LTS version Manually update engines field Auto-detected from Node.js official API
package.json exports Wrestling with documentation Perfectly generated for ESM/CommonJS/Dual
CI/CD configuration Hand-written every time, tribal knowledge Fully equipped GitHub Actions generated
Quality inconsistency Different settings per project Standardized best practices

How Dynamic Version Fetching Works

Let's look at the problem with traditional template tools.

❌ Traditional Templates

// template/package.json
const dependencies = {
  "typescript": "5.3.0",  // ← Version fixed at creation time
  "vitest": "1.0.0",      // ← Outdated in 6 months
  "tsup": "8.0.0",        // ← Becomes stale without maintenance
}
Enter fullscreen mode Exit fullscreen mode

βœ… forge-npm-pkg's Approach

async function fetchLatestVersion(packageName: string): Promise<string> {
  const response = await fetch(
    `https://registry.npmjs.org/${packageName}/latest`
  );
  const data = await response.json();
  return data.version;
}

async function fetchLatestVersions(packages: string[]): Promise<Record<string, string>> {
  const results = await Promise.all(
    packages.map(async (pkg) => [pkg, await fetchLatestVersion(pkg)])
  );
  return Object.fromEntries(results);
}
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ By dynamically fetching the latest versions from npm registry, no template maintenance is required. You always start with the latest dependencies, no matter when you use the tool.

Additionally, it warns about versions released within the last 30 days:

⚠ typescript@5.7.0 was released only 15 days ago.
  Consider using 5.6.x for stability.
Enter fullscreen mode Exit fullscreen mode

The design balances cutting-edge features with stability.

Automatic Node.js LTS Detection

Managing the Node.js engines field and CI/CD matrix manually is tedious, right?

❌ Traditional Method

{
  "engines": { "node": ">=18.0.0" }
}
Enter fullscreen mode Exit fullscreen mode

↑ You won't notice when Node.js 18 reaches EOL...

βœ… forge-npm-pkg's Method

async function fetchNodeLTSVersions(): Promise<string[]> {
  const response = await fetch('https://nodejs.org/dist/index.json');
  const releases = await response.json();

  return releases
    .filter((r: any) => r.lts)  // Extract only LTS versions
    .map((r: any) => r.version.replace('v', '').split('.')[0])
    .slice(0, 2);  // Latest 2 LTS versions (e.g., [22, 20])
}
Enter fullscreen mode Exit fullscreen mode

It fetches current LTS versions from the official Node.js API and automatically configures the engines field and GitHub Actions matrix.

Perfect package.json exports Configuration

Are you confident your package.json exports field is correct?

forge-npm-pkg generates perfect exports configuration for all three module formats: ESM, CommonJS, and Dual.

ESM (Modern)

{
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Dual (ESM + CommonJS)

{
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

It also auto-generates a validation script using @arethetypeswrong/cli:

npm run check:exports  # Validates exports configuration
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ @arethetypeswrong/cli is a tool that checks whether your package's exports configuration correctly resolves TypeScript types. It catches ESM/CJS configuration mistakes early.

Fully Equipped CI/CD

Let's look at the generated GitHub Actions workflows.

ci.yml - Automated Testing on Pull Requests

jobs:
  test:
    strategy:
      matrix:
        node-version: [20.x, 22.x]  # ← Auto-detected LTS
    steps:
      - run: npm test
      - run: npm run typecheck
      - run: npm run lint
      - run: npm run build
Enter fullscreen mode Exit fullscreen mode

publish.yml - Auto-publish on Tag Push

on:
  push:
    tags: ['v*']  # Triggers on v1.0.0 format tags

jobs:
  publish:
    steps:
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

dependabot-auto-merge.yml - Auto-merge Dependencies

# patch/minor β†’ auto-merge
# major β†’ manual review required
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ GitHub Actions versions (like actions/checkout@v4) are also fetched from the GitHub API to ensure the latest versions.

async function fetchActionVersion(action: string): Promise<string> {
  const [owner, repo] = action.split('/');
  const response = await fetch(
    `https://api.github.com/repos/${owner}/${repo}/releases/latest`
  );
  const data = await response.json();
  return data.tag_name;  // e.g., "v4"
}
Enter fullscreen mode Exit fullscreen mode

Interactive Configuration

β—† What language do you want to use?
β”‚ β—‹ TypeScript (Recommended)
β”‚ β—‹ JavaScript

β—† What module format do you want to use?
β”‚ β—‹ ESM (Modern)
β”‚ β—‹ CommonJS (Legacy)
β”‚ β—‹ Dual (ESM + CommonJS)

β—† What test runner do you want to use?
β”‚ β—‹ Vitest (Recommended)
β”‚ β—‹ Jest
β”‚ β—‹ None

β—† Enable ESLint + Prettier?
β”‚ ● Yes / β—‹ No

β—† Initialize git repository?
β”‚ ● Yes / β—‹ No

β—† Setup GitHub Actions CI/CD?
β”‚ ● Yes / β—‹ No
Enter fullscreen mode Exit fullscreen mode

Flexibly customize based on your project requirements.

Generated Project Structure

my-awesome-package/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ index.ts
β”‚   └── index.test.ts
β”œβ”€β”€ .github/
β”‚   β”œβ”€β”€ workflows/
β”‚   β”‚   β”œβ”€β”€ ci.yml
β”‚   β”‚   β”œβ”€β”€ publish.yml
β”‚   β”‚   └── dependabot-auto-merge.yml
β”‚   └── dependabot.yml
β”œβ”€β”€ package.json          # Perfect exports configuration
β”œβ”€β”€ tsconfig.json         # Strict mode enabled
β”œβ”€β”€ tsup.config.ts        # Build configuration
β”œβ”€β”€ vitest.config.ts      # Test configuration
β”œβ”€β”€ eslint.config.js      # ESLint flat config
β”œβ”€β”€ .prettierrc
β”œβ”€β”€ .gitignore
└── README.md
Enter fullscreen mode Exit fullscreen mode

All files work together in harmony. No need to worry about consistency issues from copy-pasting individual configurations.

Usage

No Installation Required (npx)

npx forge-npm-pkg my-package-name
Enter fullscreen mode Exit fullscreen mode

Global Installation

npm install -g forge-npm-pkg
forge-npm-pkg my-package-name
Enter fullscreen mode Exit fullscreen mode

Create GitHub Repository Simultaneously

# If gh CLI is installed
forge-npm-pkg my-package-name --github
Enter fullscreen mode Exit fullscreen mode

Design Philosophy: The Power of Standardization

I have experience standardizing development processes for teams at work:

  • Task management with Epic/Story/Task structure
  • PR-based development workflow
  • Automatic versioning with semantic-release

What I learned from this experience:

When you "systematize" good practices, the entire team's quality improves

forge-npm-pkg applies the same concept to "npm package development."

Define the "correct configuration" once, and anyone can create best-in-class packages anytime

This isn't just about individual productivityβ€”it leads to quality improvement across teams and organizations.

Potential for Broader Application

This approach can be applied to other areas:

Domain What Can Be Standardized
RAG Projects Vector DB settings, chunking strategies, prompt templates
Next.js Projects Auth configuration, API structure, deployment settings
AWS CDK Security settings, tagging rules, monitoring

"Repeating the same thing every time" β†’ "Templatize to ensure standards"

This pattern can be leveraged across various development domains.

Conclusion

forge-npm-pkg is a tool that makes "npm package development best practices" accessible to everyone.

βœ… 5 Problems Solved

Problem Solution
Writing the same config every time Generate everything with 1 command
Outdated dependency versions Dynamic fetching from npm registry
Complex package.json exports Perfect auto-generated configuration
Tribal knowledge in CI/CD Fully equipped GitHub Actions
Quality inconsistency Standardized best practices

βœ… Technical Highlights

  • Dynamic version fetching from npm registry API
  • Auto-detection of LTS from official Node.js API
  • Actions version fetching from GitHub API
  • Warnings for too-new versions

If you find yourself "repeating the same setup every time you start npm package development," give it a try.

npx forge-npm-pkg my-awesome-package
Enter fullscreen mode Exit fullscreen mode

πŸ“¦ NPM: https://www.npmjs.com/package/forge-npm-pkg
πŸ“‚ GitHub: https://github.com/your-username/forge-npm-pkg

Feedback and contributions are welcome!

Top comments (0)