From ccfg-typescript
This skill should be used when creating or editing package.json, tsconfig, managing npm dependencies, configuring monorepos, or publishing npm packages.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ccfg-typescript:packaging-conventionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill defines best practices for packaging, dependency management, monorepo configuration, and
This skill defines best practices for packaging, dependency management, monorepo configuration, and publishing TypeScript packages to npm with modern module formats and optimal developer experience.
ALWAYS include these core fields in package.json.
CORRECT:
{
"name": "@scope/package-name",
"version": "1.2.3",
"description": "Clear, concise package description",
"keywords": ["typescript", "library", "utility"],
"author": "Your Name <email@example.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/username/repo.git"
},
"bugs": {
"url": "https://github.com/username/repo/issues"
},
"homepage": "https://github.com/username/repo#readme"
}
ALWAYS use "type": "module" for ESM packages and configure exports properly.
CORRECT for ESM-only package:
{
"name": "@scope/package-name",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"files": ["dist"],
"sideEffects": false
}
CORRECT for dual ESM/CJS package:
{
"name": "@scope/package-name",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./package.json": "./package.json"
},
"files": ["dist"],
"sideEffects": false
}
CORRECT for multiple entry points:
{
"name": "@scope/package-name",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./utils": {
"import": {
"types": "./dist/utils.d.ts",
"default": "./dist/utils.js"
},
"require": {
"types": "./dist/utils.d.cts",
"default": "./dist/utils.cjs"
}
},
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"utils": ["./dist/utils.d.ts"]
}
}
}
Explicitly list files to publish to avoid bloat.
CORRECT:
{
"files": ["dist", "README.md", "LICENSE"]
}
WRONG:
{
"files": ["src", "dist", "tests", "node_modules"]
}
ALWAYS set sideEffects to enable tree-shaking.
CORRECT for pure library:
{
"sideEffects": false
}
CORRECT for library with some side effects:
{
"sideEffects": ["*.css", "./src/polyfills.ts"]
}
Specify packageManager for consistent dependency resolution.
CORRECT:
{
"packageManager": "pnpm@8.15.0"
}
Standardize script names across projects.
CORRECT:
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsup",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
"validate": "pnpm run typecheck && pnpm run lint && pnpm run test:coverage && pnpm run format:check",
"clean": "rm -rf dist coverage",
"prepublishOnly": "pnpm run validate && pnpm run build"
}
}
Specify supported Node.js and package manager versions.
CORRECT:
{
"engines": {
"node": ">=18.0.0",
"pnpm": ">=8.0.0"
}
}
CORRECT for publishable library:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2023"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
CORRECT:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@utils/*": ["./src/utils/*"],
"@hooks/*": ["./src/hooks/*"],
"@types/*": ["./src/types/*"]
},
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
CORRECT base config (tsconfig.base.json):
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
Package-specific tsconfig:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2023"],
"outDir": "./dist",
"rootDir": "./src",
"composite": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
CORRECT root tsconfig.json with references:
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/utils" },
{ "path": "./packages/cli" },
{ "path": "./apps/web" }
]
}
Build with references:
tsc --build --verbose
ALWAYS use tsup for library bundling (ESM + CJS).
CORRECT tsup.config.ts:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
treeshake: true,
minify: false,
target: 'es2022',
outDir: 'dist',
});
For multiple entry points:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: {
index: 'src/index.ts',
utils: 'src/utils.ts',
cli: 'src/cli.ts',
},
format: ['cjs', 'esm'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
treeshake: true,
outDir: 'dist',
});
CORRECT vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
build: {
target: 'es2022',
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
});
CORRECT build script with esbuild:
// build.ts
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/index.js',
platform: 'node',
format: 'esm',
target: 'es2022',
sourcemap: true,
external: ['node:*'],
minify: false,
});
Place dependencies in the correct field.
CORRECT:
{
"dependencies": {
"zod": "^3.22.4",
"date-fns": "^3.0.0"
},
"devDependencies": {
"typescript": "^5.3.3",
"vitest": "^1.2.0",
"@types/node": "^20.11.0",
"tsup": "^8.0.1",
"prettier": "^3.2.4",
"eslint": "^8.56.0"
},
"peerDependencies": {
"react": "^18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": false
}
}
}
WRONG:
{
"dependencies": {
"typescript": "^5.3.3",
"vitest": "^1.2.0",
"@types/node": "^20.11.0",
"react": "^18.0.0"
}
}
Use conservative version ranges for libraries, flexible for applications.
CORRECT for libraries:
{
"dependencies": {
"zod": "^3.22.4",
"date-fns": "^3.0.0"
}
}
CORRECT for applications (can be more flexible):
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^3.22.4"
}
}
ALWAYS specify peer dependencies for libraries that extend other tools.
CORRECT React component library:
{
"name": "@scope/ui-components",
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": false
}
},
"devDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
CORRECT TypeScript plugin:
{
"name": "@scope/typescript-plugin",
"peerDependencies": {
"typescript": "^5.0.0"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
ALWAYS use pnpm workspaces for monorepos.
CORRECT pnpm-workspace.yaml:
packages:
- 'packages/*'
- 'apps/*'
- 'tools/*'
CORRECT root package.json:
{
"name": "monorepo-root",
"private": true,
"packageManager": "pnpm@8.15.0",
"scripts": {
"dev": "pnpm -r --parallel dev",
"build": "pnpm -r --filter '!@scope/app-*' build",
"test": "pnpm -r test",
"lint": "pnpm -r lint",
"typecheck": "pnpm -r typecheck",
"clean": "pnpm -r clean && rm -rf node_modules"
},
"devDependencies": {
"typescript": "^5.3.3",
"prettier": "^3.2.4",
"eslint": "^8.56.0"
}
}
CORRECT cross-package dependency:
{
"name": "@scope/app-web",
"dependencies": {
"@scope/core": "workspace:*",
"@scope/ui": "workspace:*"
}
}
CORRECT turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Run with Turborepo:
pnpm turbo build
pnpm turbo test --filter=@scope/core
pnpm turbo dev --parallel
CORRECT shared TypeScript config:
monorepo/
├── tsconfig.base.json
├── packages/
│ ├── core/
│ │ └── tsconfig.json (extends ../../tsconfig.base.json)
│ └── utils/
│ └── tsconfig.json (extends ../../tsconfig.base.json)
└── apps/
└── web/
└── tsconfig.json (extends ../../tsconfig.base.json)
CORRECT shared ESLint config:
// packages/eslint-config/index.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked);
Use in packages:
// packages/core/eslint.config.js
import baseConfig from '@scope/eslint-config';
export default [
...baseConfig,
{
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
];
Before publishing, ensure:
ALWAYS use Changesets for version management.
Install:
pnpm add -D @changesets/cli
pnpm exec changeset init
CORRECT .changeset/config.json:
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
Add changeset:
pnpm exec changeset
Version packages:
pnpm exec changeset version
Publish:
pnpm exec changeset publish
CORRECT for public package:
{
"name": "@scope/package-name",
"version": "1.0.0",
"description": "Package description",
"keywords": ["typescript", "library"],
"author": "Your Name",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/username/repo.git"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"prepublishOnly": "pnpm run validate && pnpm run build"
}
}
CORRECT for private package:
{
"name": "@scope/internal-package",
"version": "1.0.0",
"private": true
}
CORRECT:
{
"scripts": {
"prepublishOnly": "pnpm run validate && pnpm run build",
"prepack": "pnpm run build",
"postpublish": "git push --follow-tags"
}
}
CORRECT .npmignore:
# Source files
src/
tests/
e2e/
**/*.test.ts
**/*.spec.ts
**/__tests__/
# Configuration
tsconfig.json
vitest.config.ts
eslint.config.js
.prettierrc
.editorconfig
# Development
.vscode/
.idea/
*.log
.env
.env.*
# Build artifacts
coverage/
.vitest/
node_modules/
# Git
.git/
.gitignore
Or use files field in package.json (preferred):
{
"files": ["dist", "README.md", "LICENSE"]
}
CORRECT:
{
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
}
}
WRONG (can cause dual package hazard):
{
"main": "./dist/index.js",
"module": "./dist/index.esm.js",
"exports": "./dist/index.js"
}
CORRECT:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
outDir: 'dist',
// Ensures .cjs and .js extensions
outExtension({ format }) {
return {
js: format === 'cjs' ? '.cjs' : '.js',
};
},
});
CORRECT README.md structure:
# @scope/package-name
Brief description of the package.
## Installation
```bash
pnpm add @scope/package-name
```
## Usage
```typescript
import { someFunction } from '@scope/package-name';
const result = someFunction('input');
```
## API
### `someFunction(input: string): string`
Description of what the function does.
**Parameters:**
- `input` - Description of parameter
**Returns:** Description of return value
**Example:**
```typescript
const result = someFunction('test');
// result: 'TEST'
```
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md)
## License
MIT © Your Name
CORRECT:
/**
* Validates an email address.
*
* @param email - The email address to validate
* @returns `true` if valid, `false` otherwise
*
* @example
* ```ts
* isValidEmail('user@example.com'); // true
* isValidEmail('invalid'); // false
* ```
*
* @public
*/
export function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
Install:
pnpm add -D typedoc
CORRECT typedoc.json:
{
"entryPoints": ["src/index.ts"],
"out": "docs",
"plugin": ["typedoc-plugin-markdown"],
"readme": "README.md",
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
}
Generate:
pnpm exec typedoc
CORRECT .github/workflows/publish.yml:
name: Publish Package
on:
push:
branches:
- main
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v2
with:
version: 8
- run: pnpm install
- run: pnpm run validate
- run: pnpm run build
- name: Create Release Pull Request or Publish
uses: changesets/action@v1
with:
publish: pnpm exec changeset publish
version: pnpm exec changeset version
commit: 'chore: version packages'
title: 'chore: version packages'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
When configuring packages:
These conventions ensure professional, maintainable npm packages.
npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-typescriptDevelops npm packages with Node.js and TypeScript: library setup, CLI tools, dual ESM/CJS exports, tsup/tsc builds, vitest testing, and versioning/dist-tag strategies.
Guides TypeScript library authoring: project setup, dual CJS/ESM package exports, tsdown/unbuild config, type-safe API design, advanced type patterns, vitest testing, and npm release workflows.
Guides monorepo workflows including local dev setup with Turborepo/pnpm, cross-package development, watch modes, CI/CD patterns, versioning, publishing, and Vite/Next.js configs.