A better npm publish tool with automated workflows, version bumping, testing, and git integration
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
General utility functions for package analysis, file operations, display formatting, and development helpers used throughout the np publishing workflow.
Functions for reading and analyzing package information.
/**
* Read package.json and determine root directory
* @param packagePath - Path to package or directory (default: process.cwd())
* @returns Promise resolving to package data and root directory
*/
function readPackage(packagePath?: string): Promise<{
package_: NormalizedPackageJson;
rootDirectory: string;
}>;
/**
* Get files that are new since the last release
* @param rootDirectory - Project root directory
* @returns Promise resolving to object with unpublished and firstTime file arrays
*/
function getNewFiles(rootDirectory: string): Promise<{
unpublished: string[]; // Files new since last release but not included in package
firstTime: string[]; // Files new since last release and included in package
}>;
/**
* Get dependencies that are new since the last release
* @param newPackage - Updated package.json object
* @param rootDirectory - Project root directory
* @returns Promise resolving to array of new dependency names
*/
function getNewDependencies(
newPackage: object,
rootDirectory: string
): Promise<string[]>;
/**
* Get npm package access information
* @param name - Package name
* @returns Promise resolving to access information
*/
function getNpmPackageAccess(name: string): Promise<{
access: 'public' | 'restricted';
[key: string]: any;
}>;Usage Examples:
import { readPackage, getNewFiles, getNewDependencies } from "np/source/util.js";
// Read package information
const { package_, rootDirectory } = await readPackage();
console.log(`Package: ${package_.name}@${package_.version}`);
console.log(`Root: ${rootDirectory}`);
// Read from specific path
const { package_: otherPkg } = await readPackage('./packages/core');
// Analyze changes since last release
const fileChanges = await getNewFiles(rootDirectory);
const newDeps = await getNewDependencies(package_, rootDirectory);
console.log(`Unpublished files: ${fileChanges.unpublished.length}`);
console.log(`First-time files: ${fileChanges.firstTime.length}`);
console.log(`New dependencies: ${newDeps.join(', ')}`);Functions for formatting text and creating user-friendly displays.
/**
* Convert issue references in text to clickable links
* @param url - Repository URL
* @param message - Text containing issue references (#123)
* @returns Text with issue references converted to links
*/
function linkifyIssues(url: string, message: string): string;
/**
* Convert commit hash to clickable link
* @param url - Repository URL
* @param commit - Commit hash
* @returns Formatted commit link
*/
function linkifyCommit(url: string, commit: string): string;
/**
* Convert commit range to clickable link
* @param url - Repository URL
* @param commitRange - Commit range (e.g., "abc123...def456")
* @returns Formatted commit range link
*/
function linkifyCommitRange(url: string, commitRange: string): string;
/**
* Join array into human-readable list with proper punctuation
* @param list - Array of strings to join
* @returns Formatted list string
*/
function joinList(list: string[]): string;
/**
* Group files by folders for compact display
* @param files - Array of file paths
* @param groupingMinimumDepth - Minimum depth to start grouping (default: 1)
* @param groupingThresholdCount - Minimum files to trigger grouping (default: 5)
* @returns Formatted string with grouped file display
*/
function groupFilesInFolders(
files: string[],
groupingMinimumDepth?: number,
groupingThresholdCount?: number
): string;Usage Examples:
import { linkifyIssues, linkifyCommit, joinList, groupFilesInFolders } from "np/source/util.js";
// Format commit messages with links
const repoUrl = "https://github.com/sindresorhus/np";
const message = "Fix issue #123 and resolve #456";
const linkedMessage = linkifyIssues(repoUrl, message);
// Result: "Fix issue [#123](https://github.com/sindresorhus/np/issues/123) and resolve [#456](https://github.com/sindresorhus/np/issues/456)"
// Format commit links
const commitLink = linkifyCommit(repoUrl, "abc123def456");
// Result: "[abc123d](https://github.com/sindresorhus/np/commit/abc123def456)"
// Format lists
const dependencies = ['react', 'lodash', 'axios'];
const depList = joinList(dependencies);
// Result: "react, lodash, and axios"
// Group files for display
const files = [
'src/index.js',
'src/utils.js',
'src/components/Button.js',
'src/components/Modal.js',
'test/index.test.js'
];
const grouped = groupFilesInFolders(files);
// Result: Formatted string with grouped files displayFunctions for validation and error handling.
/**
* Assert condition with custom error message
* @param condition - Boolean condition to assert
* @param message - Error message if condition is false
* @throws Error if condition is false
*/
function assert(condition: boolean, message: string): void;
/**
* Validate that engine version satisfies requirements
* @param engine - Engine name (node, npm, git, etc.)
* @param version - Version string to validate
* @throws Error if version doesn't satisfy engine requirements defined in np's package.json
*/
function validateEngineVersionSatisfies(engine: string, version: string): void;Usage Examples:
import { assert, validateEngineVersionSatisfies } from "np/source/util.js";
// Assertions for validation
assert(packageJson.name, 'Package name is required');
assert(!packageJson.private || !options.publish, 'Cannot publish private package');
// Engine version validation (throws on invalid versions)
try {
validateEngineVersionSatisfies('node', process.version);
validateEngineVersionSatisfies('npm', npmVersion);
console.log('All engine versions are valid');
} catch (error) {
console.error(`Engine requirement not met: ${error.message}`);
}Functions for handling configuration and naming conventions.
/**
* Get version tag prefix from package manager configuration (memoized)
* @param packageManager - Package manager configuration object
* @returns Promise resolving to tag prefix string (e.g., "v")
*/
function getTagVersionPrefix(packageManager: object): Promise<string>;
/**
* Get prerelease prefix from configuration (memoized)
* @param config - Configuration object
* @returns Promise resolving to prerelease prefix string
*/
function getPreReleasePrefix(config: object): Promise<string>;Usage Examples:
import { getTagVersionPrefix, getPreReleasePrefix } from "np/source/util.js";
// Get tag prefix for version tags
const tagPrefix = await getTagVersionPrefix(packageManagerConfig);
const tagName = `${tagPrefix}${version}`; // e.g., "v1.2.3"
// Get prerelease prefix
const prePrefix = await getPreReleasePrefix(config);
const preVersion = `${version}-${prePrefix}.1`; // e.g., "1.2.3-alpha.1"Access to np's own package information and metadata.
/**
* NP's own package.json data
*/
const npPackage: NormalizedPackageJson;
/**
* NP's root directory path
*/
const npRootDirectory: string;Usage Examples:
import { npPackage, npRootDirectory } from "np/source/util.js";
// Access np's version for update notifications
console.log(`np version: ${npPackage.version}`);
// Use np's directory for relative paths
const templatePath = path.join(npRootDirectory, 'templates', 'release.md');Advanced file analysis for publishing workflows:
// Analyze files that will be published
const fileChanges = await getNewFiles(rootDirectory);
const allNewFiles = [...fileChanges.unpublished, ...fileChanges.firstTime];
// Categorize file changes
const fileTypes = {
source: allNewFiles.filter(f => f.startsWith('src/')),
tests: allNewFiles.filter(f => f.includes('.test.') || f.includes('.spec.')),
docs: allNewFiles.filter(f => f.endsWith('.md')),
config: allNewFiles.filter(f => f.includes('config') || f.endsWith('.json'))
};
console.log(`Source files changed: ${fileTypes.source.length}`);
console.log(`Test files changed: ${fileTypes.tests.length}`);
console.log(`Unpublished files: ${fileChanges.unpublished.length}`);
console.log(`First-time files: ${fileChanges.firstTime.length}`);Track dependency changes between releases:
// Get new dependencies
const newDeps = await getNewDependencies(updatedPackage, rootDirectory);
// Categorize dependencies
const depTypes = {
production: newDeps.filter(dep =>
updatedPackage.dependencies?.[dep]
),
development: newDeps.filter(dep =>
updatedPackage.devDependencies?.[dep]
),
peer: newDeps.filter(dep =>
updatedPackage.peerDependencies?.[dep]
)
};
console.log(`New production dependencies: ${depTypes.production.join(', ')}`);Smart file grouping for readable output:
const files = [
'src/index.js',
'src/utils/helpers.js',
'src/utils/validation.js',
'src/components/Button/index.js',
'src/components/Button/Button.css',
'src/components/Modal/index.js',
'test/unit/helpers.test.js',
'test/integration/workflow.test.js'
];
// Group with default settings
const grouped = groupFilesInFolders(files);
/*
Result is a formatted string like:
- src/index.js
- src/utils/* (2 files)
- src/components/Button/* (2 files)
- src/components/Modal/index.js
- test/unit/helpers.test.js
- test/integration/workflow.test.js
*/
// Custom grouping thresholds
const customGrouped = groupFilesInFolders(files, 2, 3);
// Only groups folders with 3+ files at depth 2+Generate clickable links for various git references:
const repoUrl = "https://github.com/sindresorhus/np";
// Issue links
const issueText = "Closes #123, fixes #456";
const withIssueLinks = linkifyIssues(repoUrl, issueText);
// "Closes [#123](https://github.com/sindresorhus/np/issues/123), fixes [#456](https://github.com/sindresorhus/np/issues/456)"
// Commit links
const commitHash = "abc123def456";
const commitLink = linkifyCommit(repoUrl, commitHash);
// "[abc123d](https://github.com/sindresorhus/np/commit/abc123def456)"
// Commit range links
const range = "v1.0.0...v1.1.0";
const rangeLink = linkifyCommitRange(repoUrl, range);
// "[v1.0.0...v1.1.0](https://github.com/sindresorhus/np/compare/v1.0.0...v1.1.0)"Format arrays into readable lists:
// Simple list
joinList(['one']) // "one"
joinList(['one', 'two']) // "one and two"
joinList(['one', 'two', 'three']) // "one, two, and three"
// With different types
const files = ['package.json', 'src/index.js', 'README.md'];
console.log(`Modified files: ${joinList(files)}`);
// "Modified files: package.json, src/index.js, and README.md"Several utility functions use memoization for performance:
// These functions cache results to avoid repeated computation
getTagVersionPrefix(packageManager) // Cached per packageManager
getPreReleasePrefix(config) // Cached per config
// Manual cache clearing (if needed)
getTagVersionPrefix.cache.clear();
getPreReleasePrefix.cache.clear();Utility functions are optimized for repeated calls:
The assert function provides clear error messages:
// Custom assertions with context
assert(fs.existsSync(packagePath), `Package file not found: ${packagePath}`);
assert(semver.valid(version), `Invalid version format: ${version}`);
assert(Array.isArray(files), 'Files must be an array');
// Conditional assertions
if (options.publish && package_.private) {
assert(false, 'Cannot publish private package to npm');
}Version validation provides detailed error information:
// Engine validation with specific requirements
try {
validateEngineVersionSatisfies('node', '14.0.0');
validateEngineVersionSatisfies('npm', '6.0.0');
} catch (error) {
console.error(`Engine requirement not met: ${error.message}`);
// "Engine requirement not met: npm version 6.0.0 does not satisfy >=9.0.0"
}File utilities handle common error scenarios:
try {
const newFiles = await getNewFiles(rootDirectory);
} catch (error) {
if (error.code === 'ENOENT') {
console.warn('No git repository found, skipping file analysis');
return [];
}
throw error; // Re-throw unexpected errors
}Install with Tessl CLI
npx tessl i tessl/npm-np