@ebowwa/npm-publishing 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@ebowwa/npm-publishing",
3
+ "version": "1.0.0",
4
+ "description": "NPM package publishing, version management, and validation utilities",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "dev": "bun run --watch src/index.ts",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "semver": "^7.6.0",
15
+ "tar": "^7.4.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/bun": "latest",
19
+ "@types/node": "^20.0.0",
20
+ "@types/semver": "^7.5.0",
21
+ "@types/tar": "^6.1.0",
22
+ "typescript": "^5.0.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=20.0.0"
26
+ },
27
+ "keywords": [
28
+ "npm",
29
+ "publish",
30
+ "semver",
31
+ "registry",
32
+ "version",
33
+ "validation"
34
+ ],
35
+ "author": "ebowwa",
36
+ "license": "MIT"
37
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Bump the version in package.json
3
+ */
4
+
5
+ import { execSync } from 'child_process';
6
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
7
+ import { join } from 'path';
8
+ import semver from 'semver';
9
+ import type { BumpVersionOptions, BumpVersionResult } from './types.js';
10
+
11
+ export async function bumpVersion(options: BumpVersionOptions): Promise<BumpVersionResult> {
12
+ try {
13
+ const packagePath = options.packagePath || process.cwd();
14
+ const packageJsonPath = join(packagePath, 'package.json');
15
+
16
+ if (!existsSync(packageJsonPath)) {
17
+ return {
18
+ success: false,
19
+ oldVersion: '',
20
+ newVersion: '',
21
+ error: `package.json not found at ${packageJsonPath}`,
22
+ };
23
+ }
24
+
25
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
26
+ const oldVersion = packageJson.version || '0.0.0';
27
+
28
+ // Bump version using semver
29
+ let newVersion: string;
30
+ if (options.releaseType === 'prerelease') {
31
+ newVersion = options.preId
32
+ ? semver.inc(oldVersion, 'prerelease', options.preId) || oldVersion
33
+ : semver.inc(oldVersion, 'prerelease') || oldVersion;
34
+ } else if (options.releaseType.startsWith('pre')) {
35
+ if (options.preId) {
36
+ newVersion = semver.inc(oldVersion, options.releaseType as semver.ReleaseType, options.preId) || oldVersion;
37
+ } else {
38
+ newVersion = semver.inc(oldVersion, options.releaseType as semver.ReleaseType) || oldVersion;
39
+ }
40
+ } else {
41
+ newVersion = semver.inc(oldVersion, options.releaseType as semver.ReleaseType) || oldVersion;
42
+ }
43
+
44
+ // Update package.json
45
+ packageJson.version = newVersion;
46
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
47
+
48
+ // Run npm version to update package-lock.json if present
49
+ try {
50
+ execSync(`npm version ${newVersion} --no-git-tag-version`, {
51
+ cwd: packagePath,
52
+ stdio: 'ignore',
53
+ });
54
+ } catch {
55
+ // Ignore if npm version fails
56
+ }
57
+
58
+ return {
59
+ success: true,
60
+ oldVersion,
61
+ newVersion,
62
+ };
63
+ } catch (error) {
64
+ return {
65
+ success: false,
66
+ oldVersion: '',
67
+ newVersion: '',
68
+ error: error instanceof Error ? error.message : String(error),
69
+ };
70
+ }
71
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * NPM dist-tag operations
3
+ */
4
+
5
+ import { execSync } from 'child_process';
6
+ import type { DistTagResult, DistTagsResult } from './types.js';
7
+
8
+ export async function setDistTag(options: {
9
+ authToken: string;
10
+ packageName: string;
11
+ tag: string;
12
+ version: string;
13
+ registry?: string;
14
+ }): Promise<DistTagResult> {
15
+ try {
16
+ const args = ['npm', 'dist-tag', 'add', `${options.packageName}@${options.version}`, options.tag];
17
+
18
+ if (options.registry) {
19
+ args.push('--registry', options.registry);
20
+ }
21
+
22
+ execSync(args.join(' '), {
23
+ env: { ...process.env, NPM_TOKEN: options.authToken },
24
+ stdio: 'pipe',
25
+ });
26
+
27
+ return { success: true };
28
+ } catch (error) {
29
+ return {
30
+ success: false,
31
+ error: error instanceof Error ? error.message : String(error),
32
+ };
33
+ }
34
+ }
35
+
36
+ export async function getDistTags(
37
+ packageName: string,
38
+ authToken: string,
39
+ registry?: string
40
+ ): Promise<DistTagsResult> {
41
+ try {
42
+ const args = ['npm', 'dist-tag', 'ls', packageName];
43
+
44
+ if (registry) {
45
+ args.push('--registry', registry);
46
+ }
47
+
48
+ const output = execSync(args.join(' '), {
49
+ env: { ...process.env, NPM_TOKEN: authToken },
50
+ encoding: 'utf-8',
51
+ });
52
+
53
+ // Parse output: "latest: 1.0.0\nbeta: 1.1.0"
54
+ const tags: Record<string, string> = {};
55
+ for (const line of output.trim().split('\n')) {
56
+ const [tag, version] = line.split(':').map((s) => s.trim());
57
+ tags[tag] = version;
58
+ }
59
+
60
+ return { success: true, tags };
61
+ } catch (error) {
62
+ return {
63
+ success: false,
64
+ error: error instanceof Error ? error.message : String(error),
65
+ };
66
+ }
67
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Download and extract source files from a published npm package tarball
3
+ */
4
+
5
+ import { Readable } from 'stream';
6
+ import { createWriteStream } from 'fs';
7
+ import { join } from 'path';
8
+ import { pipeline } from 'stream/promises';
9
+ import * as tar from 'tar';
10
+ import { unlink } from 'fs/promises';
11
+ import type { TarballResult, PackageFile } from './types.js';
12
+
13
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
14
+
15
+ export async function fetchPackageTarball(
16
+ packageName: string,
17
+ version?: string,
18
+ registry?: string
19
+ ): Promise<TarballResult> {
20
+ const tempDir = '/tmp';
21
+ const tempFile = join(tempDir, `${packageName}-${version || 'latest'}.tgz`);
22
+
23
+ try {
24
+ // Get package metadata to find tarball URL
25
+ const registryUrl = registry || DEFAULT_REGISTRY;
26
+ const packageUrl = `${registryUrl}/${packageName}`;
27
+ const response = await fetch(packageUrl);
28
+
29
+ if (!response.ok) {
30
+ throw new Error(`Failed to fetch package metadata: ${response.status}`);
31
+ }
32
+
33
+ const metadata = await response.json();
34
+ const resolvedVersion = version || metadata['dist-tags'].latest;
35
+ const tarballUrl = metadata.versions[resolvedVersion].dist.tarball;
36
+
37
+ // Download tarball
38
+ const tarballResponse = await fetch(tarballUrl);
39
+ if (!tarballResponse.ok) {
40
+ throw new Error(`Failed to download tarball: ${tarballResponse.status}`);
41
+ }
42
+
43
+ // Write tarball to temp file
44
+ const writeStream = createWriteStream(tempFile);
45
+ await pipeline(Readable.fromWeb(tarballResponse.body as any), writeStream);
46
+
47
+ // Extract tarball
48
+ const files: PackageFile[] = [];
49
+ const extractPath = join(tempDir, `${packageName}-${Date.now()}`);
50
+
51
+ await tar.extract({
52
+ file: tempFile,
53
+ cwd: extractPath,
54
+ strip: 1, // Remove the 'package/' prefix
55
+ });
56
+
57
+ // Read extracted files
58
+ const { readdirSync, readFileSync, statSync } = await import('fs');
59
+ const { join: joinPath } = await import('path');
60
+
61
+ function readDirRecursive(dir: string, basePath = ''): PackageFile[] {
62
+ const result: PackageFile[] = [];
63
+ const entries = readdirSync(dir);
64
+
65
+ for (const entry of entries) {
66
+ const fullPath = joinPath(dir, entry);
67
+ const relativePath = basePath ? joinPath(basePath, entry) : entry;
68
+ const stat = statSync(fullPath);
69
+
70
+ if (stat.isDirectory()) {
71
+ result.push(...readDirRecursive(fullPath, relativePath));
72
+ } else if (stat.isFile()) {
73
+ try {
74
+ const content = readFileSync(fullPath, 'utf-8');
75
+ result.push({ path: relativePath, content });
76
+ } catch {
77
+ // Skip files that can't be read as text
78
+ }
79
+ }
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ files.push(...readDirRecursive(extractPath));
86
+
87
+ // Clean up temp file
88
+ await unlink(tempFile).catch(() => {});
89
+
90
+ return {
91
+ success: true,
92
+ version: resolvedVersion,
93
+ files,
94
+ };
95
+ } catch (error) {
96
+ // Clean up temp file on error
97
+ unlink(tempFile).catch(() => {});
98
+
99
+ return {
100
+ success: false,
101
+ error: error instanceof Error ? error.message : String(error),
102
+ };
103
+ }
104
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * NPM Publishing Library
3
+ *
4
+ * Utilities for NPM package publishing, version management,
5
+ * validation, search, and dist-tag operations.
6
+ */
7
+
8
+ // Export all functions and types
9
+ export * from './publish.js';
10
+ export * from './bump-version.js';
11
+ export * from './validate.js';
12
+ export * from './dist-tags.js';
13
+ export * from './search.js';
14
+ export * from './package-details.js';
15
+ export * from './read-files.js';
16
+ export * from './fetch-tarball.js';
17
+ export * from './types.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Get detailed information about an npm package
3
+ */
4
+
5
+ import type { PackageDetails } from './types.js';
6
+
7
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
8
+
9
+ export async function getPackageDetails(name: string): Promise<PackageDetails> {
10
+ const url = `${DEFAULT_REGISTRY}/${name}`;
11
+ const response = await fetch(url);
12
+
13
+ if (!response.ok) {
14
+ throw new Error(`Failed to fetch package details: ${response.status}`);
15
+ }
16
+
17
+ const data = await response.json();
18
+ return data;
19
+ }
package/src/publish.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Publish a package to the npm registry
3
+ */
4
+
5
+ import { execSync } from 'child_process';
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import { join } from 'path';
8
+ import type { PublishOptions, PublishResult } from './types.js';
9
+
10
+ export async function publish(options: PublishOptions): Promise<PublishResult> {
11
+ try {
12
+ const packagePath = options.packagePath || process.cwd();
13
+ const packageJsonPath = join(packagePath, 'package.json');
14
+
15
+ if (!existsSync(packageJsonPath)) {
16
+ return {
17
+ success: false,
18
+ error: `package.json not found at ${packageJsonPath}`,
19
+ };
20
+ }
21
+
22
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
23
+ const { name, version } = packageJson;
24
+
25
+ // Set npm token for this session
26
+ process.env.NPM_TOKEN = options.authToken;
27
+
28
+ const args = ['publish'];
29
+
30
+ if (options.dryRun) {
31
+ args.push('--dry-run');
32
+ }
33
+
34
+ if (options.registry) {
35
+ args.push('--registry', options.registry);
36
+ }
37
+
38
+ if (options.tag) {
39
+ args.push('--tag', options.tag);
40
+ }
41
+
42
+ if (options.access) {
43
+ args.push('--access', options.access);
44
+ }
45
+
46
+ // Execute npm publish
47
+ const output = execSync(`npm ${args.join(' ')}`, {
48
+ cwd: packagePath,
49
+ env: { ...process.env, NPM_TOKEN: options.authToken },
50
+ encoding: 'utf-8',
51
+ });
52
+
53
+ return {
54
+ success: true,
55
+ name,
56
+ version,
57
+ };
58
+ } catch (error) {
59
+ return {
60
+ success: false,
61
+ error: error instanceof Error ? error.message : String(error),
62
+ };
63
+ }
64
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Read source files from a local package directory
3
+ */
4
+
5
+ import { existsSync, readFileSync } from 'fs';
6
+ import { join } from 'path';
7
+ import type { ReadFilesResult } from './types.js';
8
+
9
+ export async function readPackageFiles(packagePath?: string): Promise<ReadFilesResult> {
10
+ try {
11
+ const path = packagePath || process.cwd();
12
+ const packageJsonPath = join(path, 'package.json');
13
+
14
+ if (!existsSync(packageJsonPath)) {
15
+ return {
16
+ success: false,
17
+ error: `package.json not found at ${packageJsonPath}`,
18
+ };
19
+ }
20
+
21
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
22
+ const filesField = packageJson.files || [];
23
+
24
+ // Determine which files to read
25
+ const filesToRead: string[] = [];
26
+
27
+ if (filesField.length > 0) {
28
+ // Use files field as filter
29
+ for (const pattern of filesField) {
30
+ if (pattern.includes('*')) {
31
+ // Wildcard patterns - simplified handling
32
+ const ext = pattern.split('*')[1] || '';
33
+ const jsFiles = ['index.js', 'index.ts', 'src/index.ts', 'dist/index.js'];
34
+ for (const file of jsFiles) {
35
+ if (file.endsWith(ext.replace(/^\./, ''))) {
36
+ filesToRead.push(file);
37
+ }
38
+ }
39
+ } else {
40
+ filesToRead.push(pattern);
41
+ }
42
+ }
43
+ } else {
44
+ // Default: read package.json and common source files
45
+ filesToRead.push('package.json', 'README.md', 'index.js', 'index.ts', 'src/index.ts');
46
+ }
47
+
48
+ const files = [];
49
+ let errorCount = 0;
50
+
51
+ for (const file of filesToRead) {
52
+ const filePath = join(path, file);
53
+ if (existsSync(filePath)) {
54
+ try {
55
+ const content = readFileSync(filePath, 'utf-8');
56
+ files.push({ path: file, content });
57
+ } catch {
58
+ errorCount++;
59
+ }
60
+ }
61
+ }
62
+
63
+ const result: ReadFilesResult = {
64
+ success: true,
65
+ files,
66
+ };
67
+
68
+ if (errorCount > 0) {
69
+ result.error = `Failed to read ${errorCount} file(s)`;
70
+ }
71
+
72
+ return result;
73
+ } catch (error) {
74
+ return {
75
+ success: false,
76
+ error: error instanceof Error ? error.message : String(error),
77
+ };
78
+ }
79
+ }
package/src/search.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Search for packages on the npm registry
3
+ */
4
+
5
+ import type { SearchOptions, SearchResult } from './types.js';
6
+
7
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
8
+
9
+ export async function search(query: string, options: SearchOptions = {}): Promise<SearchResult[]> {
10
+ const limit = options.limit || 250;
11
+ const results: SearchResult[] = [];
12
+
13
+ try {
14
+ const url = `${DEFAULT_REGISTRY}/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`;
15
+ const response = await fetch(url);
16
+
17
+ if (!response.ok) {
18
+ throw new Error(`npm registry returned ${response.status}`);
19
+ }
20
+
21
+ const data = await response.json();
22
+ const objects = data.objects || [];
23
+
24
+ for (const obj of objects) {
25
+ const pkg = obj.package;
26
+ results.push({
27
+ package: {
28
+ name: pkg.name,
29
+ version: pkg.version,
30
+ description: pkg.description,
31
+ keywords: pkg.keywords,
32
+ },
33
+ score: {
34
+ final: obj.score.final,
35
+ detail: obj.score.detail,
36
+ },
37
+ downloads: {
38
+ monthly: obj.downloads?.monthly || 0,
39
+ },
40
+ });
41
+ }
42
+
43
+ return results;
44
+ } catch (error) {
45
+ console.error('Search failed:', error);
46
+ return [];
47
+ }
48
+ }
package/src/types.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Type definitions for NPM publishing operations
3
+ */
4
+
5
+ export interface PackageFile {
6
+ path: string;
7
+ content: string;
8
+ }
9
+
10
+ export interface PublishOptions {
11
+ authToken: string;
12
+ dryRun?: boolean;
13
+ packagePath?: string;
14
+ registry?: string;
15
+ tag?: string;
16
+ access?: "public" | "restricted";
17
+ }
18
+
19
+ export interface PublishResult {
20
+ success: boolean;
21
+ name?: string;
22
+ version?: string;
23
+ error?: string;
24
+ }
25
+
26
+ export interface BumpVersionOptions {
27
+ releaseType: "major" | "minor" | "patch" | "premajor" | "preminor" | "prepatch" | "prerelease";
28
+ packagePath?: string;
29
+ preId?: string;
30
+ }
31
+
32
+ export interface BumpVersionResult {
33
+ success: boolean;
34
+ oldVersion: string;
35
+ newVersion: string;
36
+ error?: string;
37
+ }
38
+
39
+ export interface ValidationResult {
40
+ valid: boolean;
41
+ errors: string[];
42
+ warnings: string[];
43
+ }
44
+
45
+ export interface DistTagResult {
46
+ success: boolean;
47
+ error?: string;
48
+ }
49
+
50
+ export interface DistTagsResult {
51
+ success: boolean;
52
+ tags?: Record<string, string>;
53
+ error?: string;
54
+ }
55
+
56
+ export interface SearchOptions {
57
+ limit?: number;
58
+ }
59
+
60
+ export interface SearchResult {
61
+ package: {
62
+ name: string;
63
+ version: string;
64
+ description?: string;
65
+ keywords?: string[];
66
+ };
67
+ score: {
68
+ final: number;
69
+ detail: {
70
+ quality: number;
71
+ popularity: number;
72
+ maintenance: number;
73
+ };
74
+ };
75
+ downloads: {
76
+ monthly: number;
77
+ };
78
+ }
79
+
80
+ export interface PackageDetails {
81
+ name: string;
82
+ version: string;
83
+ description?: string;
84
+ license: string;
85
+ homepage?: string;
86
+ repository?: {
87
+ type: string;
88
+ url: string;
89
+ };
90
+ author?: string | {
91
+ name: string;
92
+ };
93
+ "dist-tags": Record<string, string>;
94
+ versions: Record<string, unknown>;
95
+ keywords?: string[];
96
+ }
97
+
98
+ export interface ReadFilesResult {
99
+ success: boolean;
100
+ files?: PackageFile[];
101
+ error?: string;
102
+ }
103
+
104
+ export interface TarballResult {
105
+ success: boolean;
106
+ version?: string;
107
+ files?: PackageFile[];
108
+ error?: string;
109
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Validate package.json for npm publishing requirements
3
+ */
4
+
5
+ import { existsSync, readFileSync } from 'fs';
6
+ import { join } from 'path';
7
+ import type { ValidationResult } from './types.js';
8
+
9
+ export async function validatePackage(packagePath?: string): Promise<ValidationResult> {
10
+ const errors: string[] = [];
11
+ const warnings: string[] = [];
12
+
13
+ const path = packagePath || process.cwd();
14
+ const packageJsonPath = join(path, 'package.json');
15
+
16
+ if (!existsSync(packageJsonPath)) {
17
+ errors.push('package.json not found');
18
+ return { valid: false, errors, warnings };
19
+ }
20
+
21
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
22
+
23
+ // Required fields
24
+ if (!packageJson.name) {
25
+ errors.push('Missing required field: name');
26
+ } else if (!/^[a-z0-9][a-z0-9-._]*$/.test(packageJson.name)) {
27
+ errors.push('Package name must be lowercase and can only contain letters, numbers, hyphens, underscores, and dots');
28
+ }
29
+
30
+ if (!packageJson.version) {
31
+ errors.push('Missing required field: version');
32
+ } else if (!/^\d+\.\d+\.\d+/.test(packageJson.version)) {
33
+ errors.push('Version must be a valid semver (e.g., 1.0.0)');
34
+ }
35
+
36
+ // Warnings for recommended fields
37
+ if (!packageJson.description) {
38
+ warnings.push('Missing recommended field: description');
39
+ }
40
+
41
+ if (!packageJson.author) {
42
+ warnings.push('Missing recommended field: author');
43
+ }
44
+
45
+ if (!packageJson.license) {
46
+ warnings.push('Missing recommended field: license');
47
+ }
48
+
49
+ if (!packageJson.keywords || packageJson.keywords.length === 0) {
50
+ warnings.push('Missing recommended field: keywords');
51
+ }
52
+
53
+ // Check for README
54
+ const readmeExists = existsSync(join(path, 'README.md')) || existsSync(join(path, 'README'));
55
+ if (!readmeExists) {
56
+ warnings.push('Missing README.md file');
57
+ }
58
+
59
+ // Validate files field if present
60
+ if (packageJson.files && !Array.isArray(packageJson.files)) {
61
+ errors.push('files field must be an array');
62
+ }
63
+
64
+ return {
65
+ valid: errors.length === 0,
66
+ errors,
67
+ warnings,
68
+ };
69
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }