npm Binary Packaging Skill
npm Binary Packaging Skill
How to Use This Skill
- Review the patterns and examples below
- Apply the relevant patterns to your implementation
- Follow the best practices outlined in this skill
Overview
This skill provides comprehensive patterns for packaging and publishing native binaries as npm packages. It covers persona-specific package creation (developer/user/contributor), 6-platform binary distribution, optionalDependencies pattern for platform detection, postinstall scripts for binary location, and automated version synchronization.
Quick Reference
Package Architecture
@myorg/myapp (Main package - meta-package)
├── @myorg/myapp-darwin-x64 (macOS Intel binary)
├── @myorg/myapp-darwin-arm64 (macOS Apple Silicon binary)
├── @myorg/myapp-linux-x64 (Linux x64 binary)
├── @myorg/myapp-linux-arm64 (Linux ARM64 binary)
├── @myorg/myapp-linux-x64-musl (Linux x64 musl binary)
└── @myorg/myapp-win32-x64 (Windows x64 binary)
Platform Detection Flow
┌─────────────────┐
│ npm install │
│ @myorg/myapp │
└────────┬────────┘
│
▼
┌────────────────────────────────────┐
│ optionalDependencies installation │
│ (all 6 platforms attempted) │
└────────┬───────────────────────────┘
│
▼
┌────────────────────────────────────┐
│ postinstall.js executes │
│ - Detects current platform │
│ - Locates binary in node_modules │
│ - Creates bin/ symlink/wrapper │
└────────┬───────────────────────────┘
│
▼
┌────────────────────────────────────┐
│ Binary ready at bin/myapp │
└────────────────────────────────────┘
When to Use This Skill
✅ Use when:
- Publishing Rust/Go/C++ binaries as npm packages
- Creating multi-platform CLI tools for npm distribution
- Building persona-specific packages (developer vs user vs contributor)
- Implementing optionalDependencies pattern for platform binaries
- Automating npm publish workflows for binary releases
- Synchronizing versions across main + platform packages
❌ Don't use when:
- Package is pure JavaScript/TypeScript (no native binaries)
- Only targeting a single platform (simplified packaging sufficient)
- Binaries are downloaded at runtime (different pattern needed)
- Using npm only for dependency management (not distribution)
Core Capabilities
1. Multi-Platform Package Generation
Creates 7 npm packages (1 main + 6 platform-specific) with correct package.json structure:
Main Package (@myorg/myapp):
- Meta-package that declares optionalDependencies
- Contains postinstall.js for binary location
- Provides bin entries for CLI execution
- Version synchronization across all packages
Platform Packages (@myorg/myapp-{platform}):
- Contains single native binary for target platform
- Minimal package.json with binary file reference
- Published separately to npm registry
- Installed only on matching platform via optionalDependencies
2. optionalDependencies Pattern
Implements npm's optionalDependencies mechanism for platform-specific installation:
{
"optionalDependencies": {
"@myorg/myapp-darwin-x64": "1.0.0",
"@myorg/myapp-darwin-arm64": "1.0.0",
"@myorg/myapp-linux-x64": "1.0.0",
"@myorg/myapp-linux-arm64": "1.0.0",
"@myorg/myapp-linux-x64-musl": "1.0.0",
"@myorg/myapp-win32-x64": "1.0.0"
}
}
Benefits:
- npm attempts to install all optionalDependencies
- Failures (platform mismatch) don't fail installation
- Correct platform package installed automatically
- Fallback handling for unsupported platforms
3. Postinstall Binary Location
Implements postinstall.js script to locate and configure binary:
Responsibilities:
- Detect current platform (process.platform, process.arch)
- Map to package naming convention (darwin-x64, linux-arm64, etc.)
- Search node_modules for platform package
- Create bin/ directory with symlink (Unix) or wrapper (Windows)
- Verify binary is executable
- Handle missing platform gracefully with error message
4. Persona-Specific Packaging
Creates specialized packages for different user personas:
Developer Package (@myorg/myapp-dev):
- Includes source code and build tools
- Development dependencies (testing, linting)
- Debugging symbols and source maps
- Larger download size acceptable
User Package (@myorg/myapp):
- Production-ready binary only
- Minimal dependencies
- Optimized for size and performance
- Default package for end users
Contributor Package (@myorg/myapp-contrib):
- Full source + binaries
- Contribution guidelines
- Development scripts and hooks
- Documentation for contributors
5. Automated Version Synchronization
Ensures all packages (main + 6 platforms + personas) share the same version:
Synchronization Points:
- Update version in all package.json files
- Update optionalDependencies references
- Update installation instructions with version
- Generate changelog for release
- Tag git repository with version
6. Checksum Verification
Implements SHA256 verification for downloaded binaries:
Verification Flow:
- Platform packages include SHA256SUMS file
- Postinstall script verifies binary checksum
- Installation fails if checksum mismatch
- Prevents corrupted or tampered binaries
Level 1: Essential Patterns
Basic Platform Package Structure
Directory Layout:
packages/
├── myapp/ # Main package
│ ├── package.json
│ ├── postinstall.js
│ ├── bin/
│ │ └── myapp # Created by postinstall
│ └── README.md
│
├── myapp-darwin-x64/ # macOS Intel
│ ├── package.json
│ ├── myapp # Native binary
│ └── SHA256SUMS
│
├── myapp-darwin-arm64/ # macOS Apple Silicon
│ ├── package.json
│ ├── myapp
│ └── SHA256SUMS
│
├── myapp-linux-x64/ # Linux x64
│ ├── package.json
│ ├── myapp
│ └── SHA256SUMS
│
├── myapp-linux-arm64/ # Linux ARM64
│ ├── package.json
│ ├── myapp
│ └── SHA256SUMS
│
├── myapp-linux-x64-musl/ # Linux musl
│ ├── package.json
│ ├── myapp
│ └── SHA256SUMS
│
└── myapp-win32-x64/ # Windows x64
├── package.json
├── myapp.exe
└── SHA256SUMS
Main Package (package.json)
{
"name": "@myorg/myapp",
"version": "1.0.0",
"description": "My awesome CLI tool",
"bin": {
"myapp": "bin/myapp"
},
"scripts": {
"postinstall": "node postinstall.js"
},
"optionalDependencies": {
"@myorg/myapp-darwin-x64": "1.0.0",
"@myorg/myapp-darwin-arm64": "1.0.0",
"@myorg/myapp-linux-x64": "1.0.0",
"@myorg/myapp-linux-arm64": "1.0.0",
"@myorg/myapp-linux-x64-musl": "1.0.0",
"@myorg/myapp-win32-x64": "1.0.0"
},
"files": [
"bin/",
"postinstall.js",
"README.md"
],
"engines": {
"node": ">=14.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/myorg/myapp.git"
},
"keywords": ["cli", "binary", "tool"],
"author": "Your Name",
"license": "MIT"
}
Platform Package (package.json)
{
"name": "@myorg/myapp-darwin-x64",
"version": "1.0.0",
"description": "macOS x64 binary for @myorg/myapp",
"os": ["darwin"],
"cpu": ["x64"],
"files": [
"myapp",
"SHA256SUMS"
],
"repository": {
"type": "git",
"url": "https://github.com/myorg/myapp.git"
},
"license": "MIT"
}
Basic Postinstall Script
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
// Platform detection
const PLATFORM_MAP = {
'darwin-x64': 'darwin-x64',
'darwin-arm64': 'darwin-arm64',
'linux-x64': 'linux-x64',
'linux-arm64': 'linux-arm64',
'win32-x64': 'win32-x64'
};
function detectPlatform() {
const platform = process.platform;
const arch = process.arch;
const key = `${platform}-${arch}`;
if (key === 'linux-x64') {
// Check for musl
const lddOutput = spawnSync('ldd', ['--version'], { encoding: 'utf8' });
if (lddOutput.stdout && lddOutput.stdout.includes('musl')) {
return 'linux-x64-musl';
}
}
return PLATFORM_MAP[key];
}
function findBinary(packageName) {
const nodeModulesPath = path.join(__dirname, 'node_modules', packageName);
const binaryName = process.platform === 'win32' ? 'myapp.exe' : 'myapp';
const binaryPath = path.join(nodeModulesPath, binaryName);
if (fs.existsSync(binaryPath)) {
return binaryPath;
}
return null;
}
function createBinWrapper(binaryPath) {
const binDir = path.join(__dirname, 'bin');
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
const targetPath = path.join(binDir, process.platform === 'win32' ? 'myapp.cmd' : 'myapp');
if (process.platform === 'win32') {
// Windows: Create .cmd wrapper
const wrapper = `@echo off\n"${binaryPath}" %*\n`;
fs.writeFileSync(targetPath, wrapper);
} else {
// Unix: Create symlink
if (fs.existsSync(targetPath)) {
fs.unlinkSync(targetPath);
}
fs.symlinkSync(binaryPath, targetPath);
fs.chmodSync(targetPath, 0o755);
}
console.log(`✓ Binary installed at ${targetPath}`);
}
function main() {
const platform = detectPlatform();
if (!platform) {
console.error(`✗ Unsupported platform: ${process.platform}-${process.arch}`);
process.exit(1);
}
const packageName = `@myorg/myapp-${platform}`;
const binaryPath = findBinary(packageName);
if (!binaryPath) {
console.error(`✗ Binary not found for ${packageName}`);
console.error(' This may happen if the platform package failed to install.');
process.exit(1);
}
createBinWrapper(binaryPath);
}
main();
Level 2: Production Patterns
Complete Package Generation Script
#!/usr/bin/env node
/**
* generate-npm-packages.js
*
* Generates all npm packages (main + 6 platforms) from built binaries.
*
* Usage:
* node scripts/generate-npm-packages.js --version 1.0.0 --binaries dist/
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const PLATFORMS = [
{ platform: 'darwin-x64', os: ['darwin'], cpu: ['x64'], ext: '' },
{ platform: 'darwin-arm64', os: ['darwin'], cpu: ['arm64'], ext: '' },
{ platform: 'linux-x64', os: ['linux'], cpu: ['x64'], ext: '' },
{ platform: 'linux-arm64', os: ['linux'], cpu: ['arm64'], ext: '' },
{ platform: 'linux-x64-musl', os: ['linux'], cpu: ['x64'], ext: '' },
{ platform: 'win32-x64', os: ['win32'], cpu: ['x64'], ext: '.exe' }
];
class PackageGenerator {
constructor(options) {
this.version = options.version;
this.binariesDir = options.binariesDir;
this.outputDir = options.outputDir || 'packages';
this.packageName = options.packageName || 'myapp';
this.scope = options.scope || '@myorg';
}
generate() {
console.log(`Generating npm packages v${this.version}...`);
// Create output directory
if (!fs.existsSync(this.outputDir)) {
fs.mkdirSync(this.outputDir, { recursive: true });
}
// Generate platform packages
for (const platform of PLATFORMS) {
this.generatePlatformPackage(platform);
}
// Generate main package
this.generateMainPackage();
console.log('✓ All packages generated successfully');
}
generatePlatformPackage(platform) {
const packageDir = path.join(this.outputDir, `${this.packageName}-${platform.platform}`);
const binaryName = `${this.packageName}${platform.ext}`;
const sourceBinary = path.join(this.binariesDir, `${this.packageName}-${platform.platform}${platform.ext}`);
console.log(` Creating ${this.packageName}-${platform.platform}...`);
// Create package directory
if (!fs.existsSync(packageDir)) {
fs.mkdirSync(packageDir, { recursive: true });
}
// Copy binary
if (!fs.existsSync(sourceBinary)) {
throw new Error(`Binary not found: ${sourceBinary}`);
}
fs.copyFileSync(sourceBinary, path.join(packageDir, binaryName));
// Generate checksum
const checksum = this.generateChecksum(sourceBinary);
fs.writeFileSync(
path.join(packageDir, 'SHA256SUMS'),
`${checksum} ${binaryName}\n`
);
// Generate package.json
const packageJson = {
name: `${this.scope}/${this.packageName}-${platform.platform}`,
version: this.version,
description: `${platform.platform} binary for ${this.scope}/${this.packageName}`,
os: platform.os,
cpu: platform.cpu,
files: [binaryName, 'SHA256SUMS'],
repository: {
type: 'git',
url: `https://github.com/${this.scope.replace('@', '')}/${this.packageName}.git`
},
license: 'MIT'
};
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(packageJson, null, 2) + '\n'
);
// Create README
const readme = this.generatePlatformReadme(platform);
fs.writeFileSync(path.join(packageDir, 'README.md'), readme);
console.log(` ✓ ${this.packageName}-${platform.platform}`);
}
generateMainPackage() {
const packageDir = path.join(this.outputDir, this.packageName);
console.log(` Creating ${this.packageName} (main package)...`);
// Create package directory
if (!fs.existsSync(packageDir)) {
fs.mkdirSync(packageDir, { recursive: true });
}
// Generate package.json
const optionalDependencies = {};
for (const platform of PLATFORMS) {
optionalDependencies[`${this.scope}/${this.packageName}-${platform.platform}`] = this.version;
}
const packageJson = {
name: `${this.scope}/${this.packageName}`,
version: this.version,
description: 'My awesome CLI tool',
bin: {
[this.packageName]: 'bin/' + this.packageName
},
scripts: {
postinstall: 'node postinstall.js'
},
optionalDependencies,
files: ['bin/', 'postinstall.js', 'README.md'],
engines: {
node: '>=14.0.0'
},
repository: {
type: 'git',
url: `https://github.com/${this.scope.replace('@', '')}/${this.packageName}.git`
},
keywords: ['cli', 'binary', 'tool'],
license: 'MIT'
};
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(packageJson, null, 2) + '\n'
);
// Copy postinstall script
const postinstallScript = this.generatePostinstallScript();
fs.writeFileSync(path.join(packageDir, 'postinstall.js'), postinstallScript);
fs.chmodSync(path.join(packageDir, 'postinstall.js'), 0o755);
// Create bin directory (empty, populated by postinstall)
const binDir = path.join(packageDir, 'bin');
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir);
}
// Create README
const readme = this.generateMainReadme();
fs.writeFileSync(path.join(packageDir, 'README.md'), readme);
console.log(` ✓ ${this.packageName} (main)`);
}
generateChecksum(filePath) {
const output = execSync(`shasum -a 256 "${filePath}"`, { encoding: 'utf8' });
return output.split(' ')[0];
}
generatePostinstallScript() {
return `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const PLATFORM_MAP = {
'darwin-x64': 'darwin-x64',
'darwin-arm64': 'darwin-arm64',
'linux-x64': 'linux-x64',
'linux-arm64': 'linux-arm64',
'win32-x64': 'win32-x64'
};
function detectPlatform() {
const platform = process.platform;
const arch = process.arch;
const key = \`\${platform}-\${arch}\`;
if (key === 'linux-x64') {
const lddOutput = spawnSync('ldd', ['--version'], { encoding: 'utf8' });
if (lddOutput.stdout && lddOutput.stdout.includes('musl')) {
return 'linux-x64-musl';
}
}
return PLATFORM_MAP[key];
}
function findBinary(packageName) {
const nodeModulesPath = path.join(__dirname, 'node_modules', packageName);
const binaryName = process.platform === 'win32' ? '${this.packageName}.exe' : '${this.packageName}';
const binaryPath = path.join(nodeModulesPath, binaryName);
if (fs.existsSync(binaryPath)) {
return binaryPath;
}
return null;
}
function verifyChecksum(binaryPath, packageDir) {
const sumsFile = path.join(packageDir, 'SHA256SUMS');
if (!fs.existsSync(sumsFile)) {
console.warn('⚠ No SHA256SUMS file found, skipping verification');
return true;
}
const expectedSum = fs.readFileSync(sumsFile, 'utf8').split(' ')[0];
const { spawnSync } = require('child_process');
const result = spawnSync('shasum', ['-a', '256', binaryPath], { encoding: 'utf8' });
const actualSum = result.stdout.split(' ')[0];
if (actualSum !== expectedSum) {
console.error(\`✗ Checksum mismatch for \${binaryPath}\`);
console.error(\` Expected: \${expectedSum}\`);
console.error(\` Actual: \${actualSum}\`);
return false;
}
return true;
}
function createBinWrapper(binaryPath) {
const binDir = path.join(__dirname, 'bin');
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
const targetPath = path.join(binDir, process.platform === 'win32' ? '${this.packageName}.cmd' : '${this.packageName}');
if (process.platform === 'win32') {
const wrapper = \`@echo off\\n"\${binaryPath}" %*\\n\`;
fs.writeFileSync(targetPath, wrapper);
} else {
if (fs.existsSync(targetPath)) {
fs.unlinkSync(targetPath);
}
fs.symlinkSync(binaryPath, targetPath);
fs.chmodSync(targetPath, 0o755);
}
console.log(\`✓ Binary installed at \${targetPath}\`);
}
function main() {
const platform = detectPlatform();
if (!platform) {
console.error(\`✗ Unsupported platform: \${process.platform}-\${process.arch}\`);
process.exit(1);
}
const packageName = \`${this.scope}/${this.packageName}-\${platform}\`;
const binaryPath = findBinary(packageName);
if (!binaryPath) {
console.error(\`✗ Binary not found for \${packageName}\`);
console.error(' This may happen if the platform package failed to install.');
process.exit(1);
}
const packageDir = path.dirname(binaryPath);
if (!verifyChecksum(binaryPath, packageDir)) {
process.exit(1);
}
createBinWrapper(binaryPath);
}
main();
`;
}
generatePlatformReadme(platform) {
return `# ${this.scope}/${this.packageName}-${platform.platform}
${platform.platform} binary for ${this.scope}/${this.packageName}.
**This is a platform-specific package.** You should install the main package instead:
\`\`\`bash
npm install ${this.scope}/${this.packageName}
\`\`\`
## Platform Details
- **OS:** ${platform.os.join(', ')}
- **CPU:** ${platform.cpu.join(', ')}
- **Version:** ${this.version}
## Binary Verification
This package includes SHA256 checksums for binary verification. The checksum is verified automatically during installation.
## License
MIT
`;
}
generateMainReadme() {
return `# ${this.scope}/${this.packageName}
My awesome CLI tool.
## Installation
\`\`\`bash
npm install ${this.scope}/${this.packageName}
\`\`\`
This will automatically install the correct binary for your platform.
## Supported Platforms
- macOS (Intel and Apple Silicon)
- Linux (x64 and ARM64, glibc and musl)
- Windows (x64)
## Usage
\`\`\`bash
${this.packageName} --help
\`\`\`
## Development
See [CONTRIBUTING.md](https://github.com/${this.scope.replace('@', '')}/${this.packageName}/blob/main/CONTRIBUTING.md) for development instructions.
## License
MIT
`;
}
}
// CLI
const args = process.argv.slice(2);
const options = {
version: args[args.indexOf('--version') + 1],
binariesDir: args[args.indexOf('--binaries') + 1],
outputDir: args[args.indexOf('--output') + 1],
packageName: args[args.indexOf('--name') + 1] || 'myapp',
scope: args[args.indexOf('--scope') + 1] || '@myorg'
};
if (!options.version || !options.binariesDir) {
console.error('Usage: node generate-npm-packages.js --version 1.0.0 --binaries dist/ [--output packages/] [--name myapp] [--scope @myorg]');
process.exit(1);
}
const generator = new PackageGenerator(options);
generator.generate();
GitHub Actions npm Publish Workflow
name: Publish npm Packages
on:
push:
tags: ['v*']
permissions:
contents: read
id-token: write # For npm provenance
env:
NODE_VERSION: 18
jobs:
# Stage 1: Download binaries from release workflow
download-binaries:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get version
id: version
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Download release binaries
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p dist/
gh release download "v${{ steps.version.outputs.version }}" -p '*' -D dist/
- name: Upload binaries
uses: actions/upload-artifact@v4
with:
name: binaries
path: dist/
retention-days: 1
outputs:
version: ${{ steps.version.outputs.version }}
# Stage 2: Generate all npm packages
generate-packages:
needs: download-binaries
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Download binaries
uses: actions/download-artifact@v4
with:
name: binaries
path: dist/
- name: Generate npm packages
run: |
node scripts/generate-npm-packages.js \
--version ${{ needs.download-binaries.outputs.version }} \
--binaries dist/ \
--output packages/ \
--name myapp \
--scope @myorg
- name: Upload packages
uses: actions/upload-artifact@v4
with:
name: npm-packages
path: packages/
retention-days: 1
# Stage 3: Publish platform packages
publish-platform-packages:
needs: generate-packages
runs-on: ubuntu-latest
strategy:
matrix:
platform:
- darwin-x64
- darwin-arm64
- linux-x64
- linux-arm64
- linux-x64-musl
- win32-x64
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Download packages
uses: actions/download-artifact@v4
with:
name: npm-packages
path: packages/
- name: Publish @myorg/myapp-${{ matrix.platform }}
working-directory: packages/myapp-${{ matrix.platform }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
npm publish --provenance --access public
# Stage 4: Publish main package (after all platforms published)
publish-main-package:
needs: publish-platform-packages
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Download packages
uses: actions/download-artifact@v4
with:
name: npm-packages
path: packages/
- name: Publish @myorg/myapp
working-directory: packages/myapp
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
npm publish --provenance --access public
- name: Verify installation
run: |
npx @myorg/myapp@latest --version
Level 3: Advanced Patterns
Persona-Specific Packages
Create specialized packages for different user types:
// generate-persona-packages.js
class PersonaPackageGenerator extends PackageGenerator {
generateDeveloperPackage() {
const packageDir = path.join(this.outputDir, `${this.packageName}-dev`);
const packageJson = {
name: `${this.scope}/${this.packageName}-dev`,
version: this.version,
description: 'Developer package with source code and build tools',
main: 'src/index.js',
bin: {
[this.packageName]: 'bin/' + this.packageName,
[`${this.packageName}-dev`]: 'bin/dev'
},
scripts: {
build: 'cargo build --release',
test: 'cargo test',
lint: 'cargo clippy',
format: 'cargo fmt'
},
devDependencies: {
// Testing and development tools
},
dependencies: {
[`${this.scope}/${this.packageName}`]: this.version
},
files: ['src/', 'tests/', 'Cargo.toml', 'build.rs', 'bin/', 'README.md'],
repository: {
type: 'git',
url: `https://github.com/${this.scope.replace('@', '')}/${this.packageName}.git`,
directory: 'packages/dev'
},
license: 'MIT'
};
// Include source code, tests, build configuration
this.copyDirectory(path.join(this.sourceDir, 'src'), path.join(packageDir, 'src'));
this.copyDirectory(path.join(this.sourceDir, 'tests'), path.join(packageDir, 'tests'));
fs.copyFileSync(path.join(this.sourceDir, 'Cargo.toml'), path.join(packageDir, 'Cargo.toml'));
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(packageJson, null, 2) + '\n'
);
}
generateContributorPackage() {
const packageDir = path.join(this.outputDir, `${this.packageName}-contrib`);
const packageJson = {
name: `${this.scope}/${this.packageName}-contrib`,
version: this.version,
description: 'Contributor package with full source and development environment',
dependencies: {
[`${this.scope}/${this.packageName}-dev`]: this.version
},
scripts: {
setup: 'node scripts/setup-dev-env.js',
'check-commit': 'node scripts/check-commit-msg.js',
'pre-commit': 'cargo fmt && cargo clippy',
'prepare-pr': 'node scripts/prepare-pr.js'
},
files: [
'CONTRIBUTING.md',
'CODE_OF_CONDUCT.md',
'.github/',
'docs/',
'scripts/',
'examples/'
],
repository: {
type: 'git',
url: `https://github.com/${this.scope.replace('@', '')}/${this.packageName}.git`
},
license: 'MIT'
};
// Include contribution guidelines, examples, development scripts
this.generateContributorDocs(packageDir);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(packageJson, null, 2) + '\n'
);
}
}
Version Synchronization Automation
// sync-versions.js
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
class VersionSynchronizer {
constructor(newVersion, packagesDir) {
this.newVersion = newVersion;
this.packagesDir = packagesDir;
}
sync() {
console.log(`Synchronizing all packages to version ${this.newVersion}...`);
// Update all package.json files
const packages = fs.readdirSync(this.packagesDir);
for (const pkg of packages) {
const packageJsonPath = path.join(this.packagesDir, pkg, 'package.json');
if (!fs.existsSync(packageJsonPath)) continue;
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson.version = this.newVersion;
// Update optionalDependencies versions in main package
if (packageJson.optionalDependencies) {
for (const dep in packageJson.optionalDependencies) {
if (dep.includes(this.packageName)) {
packageJson.optionalDependencies[dep] = this.newVersion;
}
}
}
fs.writeFileSync(
packageJsonPath,
JSON.stringify(packageJson, null, 2) + '\n'
);
console.log(` ✓ ${pkg}`);
}
// Update installation docs
this.updateDocs();
// Commit changes
this.commitChanges();
console.log('✓ Version synchronization complete');
}
updateDocs() {
// Update version references in README files
const readmePaths = [
'README.md',
'docs/INSTALLATION.md',
'packages/myapp/README.md'
];
for (const readmePath of readmePaths) {
if (!fs.existsSync(readmePath)) continue;
let content = fs.readFileSync(readmePath, 'utf8');
// Replace version references
content = content.replace(
/@myorg\/myapp@[\d.]+/g,
`@myorg/myapp@${this.newVersion}`
);
fs.writeFileSync(readmePath, content);
}
}
commitChanges() {
execSync('git add packages/*/package.json README.md docs/', { stdio: 'inherit' });
execSync(`git commit -m "chore: Bump version to ${this.newVersion}"`, { stdio: 'inherit' });
execSync(`git tag v${this.newVersion}`, { stdio: 'inherit' });
}
}
// CLI
const newVersion = process.argv[2];
if (!newVersion) {
console.error('Usage: node sync-versions.js 1.0.0');
process.exit(1);
}
const synchronizer = new VersionSynchronizer(newVersion, 'packages');
synchronizer.sync();
Enhanced Postinstall with Fallback
#!/usr/bin/env node
/**
* Advanced postinstall script with:
* - Platform detection (including musl detection)
* - Checksum verification
* - Fallback download from CDN
* - Graceful error handling
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const { spawnSync } = require('child_process');
const crypto = require('crypto');
const PACKAGE_NAME = 'myapp';
const SCOPE = '@myorg';
const CDN_BASE = 'https://cdn.example.com/releases';
class BinaryInstaller {
constructor() {
this.platform = this.detectPlatform();
this.version = this.getVersion();
}
detectPlatform() {
const platform = process.platform;
const arch = process.arch;
const key = `${platform}-${arch}`;
// Check for musl on Linux
if (key === 'linux-x64') {
const lddOutput = spawnSync('ldd', ['--version'], { encoding: 'utf8' });
if (lddOutput.stdout && lddOutput.stdout.includes('musl')) {
return 'linux-x64-musl';
}
}
const PLATFORM_MAP = {
'darwin-x64': 'darwin-x64',
'darwin-arm64': 'darwin-arm64',
'linux-x64': 'linux-x64',
'linux-arm64': 'linux-arm64',
'win32-x64': 'win32-x64'
};
return PLATFORM_MAP[key];
}
getVersion() {
const packageJsonPath = path.join(__dirname, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return packageJson.version;
}
async install() {
console.log(`Installing ${PACKAGE_NAME} v${this.version} for ${this.platform}...`);
if (!this.platform) {
console.error(`✗ Unsupported platform: ${process.platform}-${process.arch}`);
console.error('Supported platforms: darwin-x64, darwin-arm64, linux-x64, linux-arm64, linux-x64-musl, win32-x64');
process.exit(1);
}
// Try to find binary in optional dependency
let binaryPath = this.findBinaryInNodeModules();
if (!binaryPath) {
console.warn('⚠ Platform package not found, attempting CDN download...');
binaryPath = await this.downloadFromCDN();
}
if (!binaryPath) {
console.error('✗ Failed to install binary');
process.exit(1);
}
// Verify checksum
if (!this.verifyChecksum(binaryPath)) {
console.error('✗ Checksum verification failed');
process.exit(1);
}
// Create bin wrapper
this.createBinWrapper(binaryPath);
console.log('✓ Installation complete');
}
findBinaryInNodeModules() {
const packageName = `${SCOPE}/${PACKAGE_NAME}-${this.platform}`;
const nodeModulesPath = path.join(__dirname, 'node_modules', packageName);
const binaryName = process.platform === 'win32' ? `${PACKAGE_NAME}.exe` : PACKAGE_NAME;
const binaryPath = path.join(nodeModulesPath, binaryName);
if (fs.existsSync(binaryPath)) {
console.log(`✓ Found binary in ${packageName}`);
return binaryPath;
}
return null;
}
async downloadFromCDN() {
const binaryName = process.platform === 'win32' ? `${PACKAGE_NAME}.exe` : PACKAGE_NAME;
const url = `${CDN_BASE}/v${this.version}/${PACKAGE_NAME}-${this.platform}${process.platform === 'win32' ? '.exe' : ''}`;
const downloadPath = path.join(__dirname, 'bin', binaryName);
console.log(`Downloading from ${url}...`);
try {
await this.downloadFile(url, downloadPath);
console.log('✓ Download complete');
return downloadPath;
} catch (error) {
console.error(`✗ Download failed: ${error.message}`);
return null;
}
}
downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const destDir = path.dirname(dest);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
const file = fs.createWriteStream(dest);
https.get(url, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}`));
return;
}
response.pipe(file);
file.on('finish', () => {
file.close();
fs.chmodSync(dest, 0o755);
resolve();
});
}).on('error', (error) => {
fs.unlinkSync(dest);
reject(error);
});
});
}
verifyChecksum(binaryPath) {
const sumsFile = path.join(path.dirname(binaryPath), 'SHA256SUMS');
if (!fs.existsSync(sumsFile)) {
console.warn('⚠ No SHA256SUMS file, skipping verification');
return true;
}
const expectedSum = fs.readFileSync(sumsFile, 'utf8').split(' ')[0];
const fileBuffer = fs.readFileSync(binaryPath);
const actualSum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
if (actualSum !== expectedSum) {
console.error(`Checksum mismatch:`);
console.error(` Expected: ${expectedSum}`);
console.error(` Actual: ${actualSum}`);
return false;
}
console.log('✓ Checksum verified');
return true;
}
createBinWrapper(binaryPath) {
const binDir = path.join(__dirname, 'bin');
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
const targetPath = path.join(binDir, process.platform === 'win32' ? `${PACKAGE_NAME}.cmd` : PACKAGE_NAME);
if (process.platform === 'win32') {
const wrapper = `@echo off\n"${binaryPath}" %*\n`;
fs.writeFileSync(targetPath, wrapper);
} else {
if (fs.existsSync(targetPath)) {
fs.unlinkSync(targetPath);
}
fs.symlinkSync(binaryPath, targetPath);
fs.chmodSync(targetPath, 0o755);
}
console.log(`✓ Binary available at ${targetPath}`);
}
}
// Main
const installer = new BinaryInstaller();
installer.install().catch((error) => {
console.error('Installation failed:', error);
process.exit(1);
});
Integration Points
Works with:
binary-release-workflowskill - Generates binaries for npm packagingcode-signingskill - Signs binaries before npm publishsha256-verificationskill - Verifies binary integrity
Calls:
- Bash tool for binary operations and npm publish
- Write tool for package.json generation
- Read tool for version synchronization
Success Criteria
npm binary packaging is complete when:
- All 7 packages generated (1 main + 6 platforms)
- Platform packages contain correct binaries with checksums
- Main package has correct optionalDependencies
- Postinstall script successfully locates and configures binary
- All packages published to npm registry
- Installation tested on all 6 platforms
-
npm install @myorg/myappinstalls correct binary - Binary executable via npx or global install
- Version synchronized across all packages
Common Issues
Platform package installation fails:
# Check optionalDependencies format
cat packages/myapp/package.json | jq .optionalDependencies
# Verify platform package exists on npm
npm view @myorg/myapp-darwin-x64
Postinstall script can't find binary:
# Check node_modules structure
ls -la node_modules/@myorg/
# Verify platform detection
node -e "console.log(process.platform, process.arch)"
Checksum verification fails:
# Regenerate checksums
shasum -a 256 myapp > SHA256SUMS
# Verify checksum file format
cat SHA256SUMS
# Should be: <hash> <filename>
npm publish fails with 403:
# Verify npm authentication
npm whoami
# Check package scope/organization access
npm access ls-packages @myorg
# Re-authenticate
npm login
Additional Resources
- scripts/generate-npm-packages.js: Complete package generation
- scripts/sync-versions.js: Version synchronization automation
- .github/workflows/publish-npm.yml: npm publish automation
- examples/: Installation verification scripts
Success Output
When successful, this skill MUST output:
✅ SKILL COMPLETE: npm-binary-packaging
Completed:
- [x] Main package generated (@myorg/myapp)
- [x] 6 platform packages created (darwin-x64, darwin-arm64, linux-x64, linux-arm64, linux-x64-musl, win32-x64)
- [x] optionalDependencies configured correctly
- [x] postinstall.js created with platform detection
- [x] SHA256 checksums generated for all binaries
- [x] All packages published to npm registry
- [x] Installation tested on all platforms
Outputs:
- packages/myapp/package.json (main)
- packages/myapp/postinstall.js
- packages/myapp-{platform}/package.json (×6)
- packages/myapp-{platform}/SHA256SUMS (×6)
Verification:
- npm view @myorg/myapp (published ✓)
- npm install @myorg/myapp (platform binary detected ✓)
- npx @myorg/myapp --version (executable ✓)
Completion Checklist
Before marking this skill as complete, verify:
- All 7 package directories created (1 main + 6 platforms)
- Main package.json has correct optionalDependencies entries
- Each platform package has correct os/cpu restrictions
- Binaries copied to platform packages with correct names
- SHA256SUMS files generated and match binaries
- postinstall.js detects platform correctly (including musl)
- postinstall.js creates bin/ wrapper (symlink or .cmd)
- Version synchronized across all packages
- All 7 packages published to npm (main published last)
- Test installation on at least 3 platforms
- Binary executable after global install:
npm install -g @myorg/myapp
Failure Indicators
This skill has FAILED if:
- ❌ Platform package installation fails (optionalDependencies error)
- ❌ postinstall.js cannot find platform binary in node_modules
- ❌ Checksum verification fails (binary corrupted)
- ❌ Binary not executable (permission issues, missing wrapper)
- ❌ npm publish fails (authentication, package name conflict)
- ❌ Version mismatch between main and platform packages
- ❌ Wrong binary installed (platform detection logic error)
- ❌ Musl vs glibc detection incorrect on Linux
When NOT to Use
Do NOT use this skill when:
- Package is pure JavaScript/TypeScript (no native binaries needed)
- Only targeting single platform (simplified packaging sufficient)
- Binaries downloaded at runtime (different distribution pattern)
- Using electron or other bundling approach (different packaging model)
- Package not intended for npm (Docker, homebrew, apt, etc.)
- Binary size >100MB (npm package size limits)
Use alternatives:
- electron-builder - For Electron app packaging
- pkg - For bundling Node.js apps as single binary
- docker-packaging - For container-based distribution
- github-releases - For direct binary downloads
- cargo-publish - For Rust crates (native packaging)
Anti-Patterns (Avoid)
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Including all binaries in main package | 600MB+ download for all platforms | Use optionalDependencies pattern |
| No checksum verification | Corrupted binaries undetected | Always include SHA256SUMS |
| Hardcoded binary paths | Breaks on different platforms | Use platform detection in postinstall |
| Publishing main before platforms | Installation fails (404 on platform packages) | Publish platforms first, main last |
| Forgetting musl detection | Wrong binary on Alpine Linux | Check ldd output for musl |
| No fallback download | Installation fails if platform package unavailable | Implement CDN fallback in postinstall |
| Version drift | Platform packages outdated | Use sync-versions.js automation |
| No provenance | Supply chain security concerns | Use npm publish --provenance |
Principles
This skill embodies:
- #1 Search Before Create - Uses established npm patterns (optionalDependencies)
- #5 Eliminate Ambiguity - Platform detection logic handles all edge cases (musl, arch)
- #6 Clear, Understandable, Explainable - README explains installation flow clearly
- #8 No Assumptions - Postinstall validates binary exists before creating wrapper
- Automation - Package generation, version sync, and publishing fully automated
- Security - Checksums, provenance, and binary verification
Standard: CODITECT-STANDARD-AUTOMATION.md
Version: 1.1.0 | Updated: 2026-01-04 | Quality Standard Applied