Skip to main content

Agent Skills Framework Extension

npm Packaging Patterns Skill

When to Use This Skill​

Use this skill when implementing npm packaging patterns patterns in your codebase.

How to Use This Skill​

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

npm distribution strategies, platform-specific binaries, optionalDependencies, and automated version management.

Core Capabilities​

  1. Binary Packaging - Platform-specific binary distribution via npm
  2. Optional Dependencies - Graceful platform-specific package handling
  3. Monorepo Management - Multi-package coordination
  4. Version Automation - Semantic release and changelog generation
  5. Registry Publishing - Automated npm publishing workflows

Platform-Specific Binary Package​

{
"name": "my-native-tool",
"version": "1.0.0",
"description": "Cross-platform CLI tool with native binaries",
"bin": {
"mytool": "bin/mytool.js"
},
"scripts": {
"postinstall": "node scripts/postinstall.js",
"prepare": "node scripts/prepare.js"
},
"optionalDependencies": {
"@my-native-tool/darwin-x64": "1.0.0",
"@my-native-tool/darwin-arm64": "1.0.0",
"@my-native-tool/linux-x64": "1.0.0",
"@my-native-tool/linux-arm64": "1.0.0",
"@my-native-tool/win32-x64": "1.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/org/my-native-tool"
},
"engines": {
"node": ">=14.0.0"
},
"keywords": [
"cli",
"native",
"cross-platform"
]
}
// scripts/postinstall.js - Binary installation handler
const { existsSync, mkdirSync, copyFileSync, chmodSync, unlinkSync } = require('fs');
const { join } = require('path');
const { platform, arch } = process;

const PLATFORM_PACKAGES = {
'darwin-x64': '@my-native-tool/darwin-x64',
'darwin-arm64': '@my-native-tool/darwin-arm64',
'linux-x64': '@my-native-tool/linux-x64',
'linux-arm64': '@my-native-tool/linux-arm64',
'win32-x64': '@my-native-tool/win32-x64',
};

function getPlatformKey() {
const key = `${platform}-${arch}`;
if (!(key in PLATFORM_PACKAGES)) {
throw new Error(
`Unsupported platform: ${platform}-${arch}\n` +
`Supported platforms: ${Object.keys(PLATFORM_PACKAGES).join(', ')}`
);
}
return key;
}

function getBinaryPath() {
const platformKey = getPlatformKey();
const packageName = PLATFORM_PACKAGES[platformKey];

try {
const packagePath = require.resolve(`${packageName}/package.json`);
const packageDir = join(packagePath, '..');
const binaryName = platform === 'win32' ? 'mytool.exe' : 'mytool';
return join(packageDir, binaryName);
} catch (e) {
console.error(`āŒ Failed to locate platform package: ${packageName}`);
console.error(' This usually means the optional dependency failed to install.');
console.error(' Try: npm install --force');
process.exit(1);
}
}

function installBinary() {
const sourcePath = getBinaryPath();
const binDir = join(__dirname, '..', 'bin');
const targetName = platform === 'win32' ? 'mytool.exe' : 'mytool';
const targetPath = join(binDir, targetName);

// Create bin directory
if (!existsSync(binDir)) {
mkdirSync(binDir, { recursive: true });
}

// Remove old binary if exists
if (existsSync(targetPath)) {
unlinkSync(targetPath);
}

// Copy binary
copyFileSync(sourcePath, targetPath);

// Make executable (Unix only)
if (platform !== 'win32') {
chmodSync(targetPath, 0o755);
}

console.log(`āœ… Installed mytool for ${platform}-${arch}`);
}

// Only run during npm install (not during npm publish)
if (require.main === module) {
try {
installBinary();
} catch (error) {
console.error('āŒ Installation failed:', error.message);
process.exit(1);
}
}

module.exports = { installBinary, getBinaryPath };
// bin/mytool.js - Binary wrapper
#!/usr/bin/env node
const { spawn } = require('child_process');
const { join } = require('path');
const { platform } = process;

const binaryName = platform === 'win32' ? 'mytool.exe' : 'mytool';
const binaryPath = join(__dirname, binaryName);

// Execute native binary with all arguments
const child = spawn(binaryPath, process.argv.slice(2), {
stdio: 'inherit',
windowsHide: true
});

child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code);
}
});

Platform Package Template​

// packages/darwin-x64/package.json
{
"name": "@my-native-tool/darwin-x64",
"version": "1.0.0",
"description": "mytool binary for macOS x64",
"os": ["darwin"],
"cpu": ["x64"],
"repository": {
"type": "git",
"url": "https://github.com/org/my-native-tool"
},
"files": [
"mytool"
]
}

Monorepo Package Management​

// lerna.json - Lerna configuration for monorepo
{
"version": "independent",
"npmClient": "npm",
"command": {
"publish": {
"conventionalCommits": true,
"message": "chore(release): publish",
"registry": "https://registry.npmjs.org/",
"allowBranch": "main",
"ignoreChanges": [
"**/__tests__/**",
"**/*.md"
]
},
"version": {
"allowBranch": "main",
"message": "chore(release): version"
}
},
"packages": [
"packages/*"
]
}
// package.json - Root monorepo package
{
"name": "my-native-tool-monorepo",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"build": "lerna run build",
"test": "lerna run test",
"publish": "lerna publish",
"version": "lerna version",
"clean": "lerna clean -y && rm -rf node_modules"
},
"devDependencies": {
"lerna": "^6.0.0"
}
}

Semantic Release Configuration​

// release.config.js - Semantic release automation
module.exports = {
branches: ['main'],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
[
'@semantic-release/changelog',
{
changelogFile: 'CHANGELOG.md'
}
],
[
'@semantic-release/npm',
{
npmPublish: true,
tarballDir: 'dist'
}
],
[
'@semantic-release/git',
{
assets: ['package.json', 'CHANGELOG.md'],
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
}
],
[
'@semantic-release/github',
{
assets: [
{ path: 'dist/*.tgz', label: 'npm package' }
]
}
]
]
};

Publishing Workflow​

# .github/workflows/publish.yml
name: Publish to npm

on:
push:
branches:
- main

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
run: npm ci

- name: Build packages
run: npm run build

- name: Run tests
run: npm test

- name: Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release

publish-platform-packages:
needs: publish
strategy:
matrix:
include:
- os: macos-latest
platform: darwin
arch: x64
- os: macos-latest
platform: darwin
arch: arm64
- os: ubuntu-latest
platform: linux
arch: x64
- os: ubuntu-latest
platform: linux
arch: arm64
- os: windows-latest
platform: win32
arch: x64

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'

- name: Build binary
run: |
# Build native binary for platform
cargo build --release --target ${{ matrix.platform }}-${{ matrix.arch }}

- name: Package binary
run: |
cd packages/${{ matrix.platform }}-${{ matrix.arch }}
npm version ${{ needs.publish.outputs.version }}
npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Version Synchronization Script​

#!/usr/bin/env node
// scripts/sync-versions.js - Sync versions across monorepo
const fs = require('fs');
const path = require('path');
const glob = require('glob');

function syncVersions(newVersion) {
// Update root package.json
const rootPkgPath = path.join(__dirname, '..', 'package.json');
const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, 'utf8'));
rootPkg.version = newVersion;
fs.writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2) + '\n');

// Update all package/*.json
const packagePaths = glob.sync('packages/*/package.json');

for (const pkgPath of packagePaths) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
pkg.version = newVersion;

// Update dependencies on sibling packages
if (pkg.optionalDependencies) {
for (const dep in pkg.optionalDependencies) {
if (dep.startsWith('@my-native-tool/')) {
pkg.optionalDependencies[dep] = newVersion;
}
}
}

fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
}

console.log(`āœ… Synced all packages to version ${newVersion}`);
}

// CLI usage
if (require.main === module) {
const newVersion = process.argv[2];
if (!newVersion) {
console.error('Usage: node sync-versions.js <version>');
process.exit(1);
}

syncVersions(newVersion);
}

module.exports = { syncVersions };

Package Validation Script​

#!/usr/bin/env node
// scripts/validate-packages.js - Validate package integrity
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

function validatePackage(packageDir) {
const pkgPath = path.join(packageDir, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));

const errors = [];

// Validate required fields
const requiredFields = ['name', 'version', 'description', 'repository'];
for (const field of requiredFields) {
if (!pkg[field]) {
errors.push(`Missing required field: ${field}`);
}
}

// Validate platform packages have os/cpu constraints
if (pkg.name.includes('darwin') || pkg.name.includes('linux') || pkg.name.includes('win32')) {
if (!pkg.os || !pkg.cpu) {
errors.push('Platform package missing os/cpu constraints');
}
}

// Validate binary exists
if (pkg.name.includes('darwin') || pkg.name.includes('linux')) {
const binaryPath = path.join(packageDir, 'mytool');
if (!fs.existsSync(binaryPath)) {
errors.push('Binary file not found');
} else {
// Check executable permission
const stats = fs.statSync(binaryPath);
if ((stats.mode & 0o111) === 0) {
errors.push('Binary is not executable');
}
}
}

// Validate package can be packed
try {
execSync('npm pack --dry-run', { cwd: packageDir, stdio: 'pipe' });
} catch (e) {
errors.push('npm pack validation failed');
}

return errors;
}

function validateAllPackages() {
const glob = require('glob');
const packageDirs = glob.sync('packages/*');

let totalErrors = 0;

for (const pkgDir of packageDirs) {
console.log(`\nšŸ“¦ Validating: ${pkgDir}`);
const errors = validatePackage(pkgDir);

if (errors.length > 0) {
console.error(` āŒ ${errors.length} error(s):`);
errors.forEach(err => console.error(` - ${err}`));
totalErrors += errors.length;
} else {
console.log(' āœ… Valid');
}
}

if (totalErrors > 0) {
console.error(`\nāŒ Total errors: ${totalErrors}`);
process.exit(1);
} else {
console.log('\nāœ… All packages valid');
}
}

validateAllPackages();

Usage Examples​

Setup Binary Package Structure​

Apply npm-packaging-patterns skill to create npm package with platform-specific binary optionalDependencies

Configure Semantic Release​

Apply npm-packaging-patterns skill to configure semantic-release with automated version bumping and changelog generation

Validate and Publish Monorepo​

Apply npm-packaging-patterns skill to validate monorepo packages and publish to npm registry with version synchronization

Integration Points​

  • binary-distribution-patterns - Cross-platform binary builds
  • native-installer-patterns - Installation scripts
  • cicd-pipeline-design - Automated publishing workflows

Success Output​

When successful, this skill MUST output:

āœ… SKILL COMPLETE: npm-packaging-patterns

Completed:
- [x] Platform-specific packages created (darwin-x64, darwin-arm64, linux-x64, linux-arm64, win32-x64)
- [x] Binary wrapper (bin/mytool.js) implemented
- [x] Postinstall script (scripts/postinstall.js) configured
- [x] optionalDependencies structure established
- [x] Monorepo configuration (lerna.json or workspaces) set up
- [x] Semantic release automation configured
- [x] Publishing workflow (.github/workflows/publish.yml) created
- [x] Version synchronization script functional
- [x] Package validation passed

Outputs:
- packages/ directory with platform packages
- bin/mytool.js wrapper script
- scripts/postinstall.js binary installer
- .github/workflows/publish.yml CI/CD workflow
- scripts/sync-versions.js version manager
- scripts/validate-packages.js validator

Validation Results:
- All platform packages have os/cpu constraints: PASS
- Binaries exist and are executable: PASS
- package.json fields complete: PASS
- npm pack dry-run: PASS
- Version synchronization: PASS

Next Steps:
- Test installation: npm install -g .
- Verify binary execution: mytool --version
- Push to GitHub to trigger publish workflow
- Monitor npm registry for published packages

Completion Checklist​

Before marking this skill as complete, verify:

  • All platform package directories exist: packages/{darwin-x64,darwin-arm64,linux-x64,linux-arm64,win32-x64}/
  • Each platform package has correct os/cpu constraints in package.json
  • Binary files exist in each platform package
  • Binaries are executable (chmod 755 on Unix, .exe on Windows)
  • bin/mytool.js wrapper executes without errors
  • scripts/postinstall.js installs correct binary for platform
  • optionalDependencies list all platform packages
  • Semantic release configured (release.config.js)
  • Publishing workflow includes all platforms
  • scripts/validate-packages.js runs without errors
  • npm pack --dry-run succeeds for all packages

Failure Indicators​

This skill has FAILED if:

  • āŒ Platform packages missing os/cpu constraints
  • āŒ Binary files not found in platform packages
  • āŒ Binaries not executable (permission errors)
  • āŒ Postinstall script errors on any platform
  • āŒ optionalDependencies version mismatch
  • āŒ Wrapper script fails to spawn binary
  • āŒ Semantic release configuration invalid
  • āŒ Publishing workflow syntax errors
  • āŒ Version synchronization script breaks package.json
  • āŒ npm pack validation fails for any package

When NOT to Use​

Do NOT use this skill when:

  • Package is pure JavaScript (no native binaries) - use standard npm package
  • Only targeting single platform - no need for platform-specific packages
  • Binary is small enough to bundle in single package (<10MB) - use single package
  • Using different distribution method (Docker, Homebrew, apt) - not npm
  • Package is private/internal only - simpler packaging sufficient
  • Need immediate binary updates without npm publish - use direct download
  • Working with Node.js native addons - use node-gyp/prebuildify instead
  • Building Electron app - use electron-builder

Use alternative approaches:

  • Single package with bundled binaries - For small binaries
  • GitHub Releases - For direct binary downloads
  • Docker - For containerized distribution
  • Homebrew/apt - For OS-specific package managers

Anti-Patterns (Avoid)​

Anti-PatternProblemSolution
Bundling all binaries in single packageHuge package size (100MB+)Use optionalDependencies for platform-specific packages
Missing os/cpu constraintsInstalls on unsupported platformsAdd "os": ["darwin"], "cpu": ["x64"] to each package
Hardcoded platform detectionBreaks on new platformsUse process.platform and process.arch
Not handling installation failuresSilent failures, broken installsAdd try-catch in postinstall with helpful error messages
Manual version bumping across packagesVersion drift, inconsistencyUse scripts/sync-versions.js automation
Skipping binary validationShip broken/non-executable binariesRun scripts/validate-packages.js in CI
Publishing without testingBroken packages on npmTest with npm pack and local install first
Not using semantic-releaseManual changelogs, version errorsAutomate with semantic-release and conventional commits

Principles​

This skill embodies:

  • #2 Full Automation - Automated version bumping, publishing, validation
  • #3 Search Before Create - Reuse npm packaging patterns and workflows
  • #5 Eliminate Ambiguity - Explicit platform constraints (os/cpu)
  • #6 Clear, Understandable, Explainable - Well-documented package structure
  • #8 No Assumptions - Validate packages before publishing
  • #10 Keep It Simple, Stupid (KISS) - Simple wrapper script, clear directory structure

Full Principles: CODITECT-STANDARD-AUTOMATION.md