build-a-npm
Version:
A tool to create and publish Node packages with automatic version bumping and GitHub integration.
227 lines (216 loc) • 7.07 kB
JavaScript
const fs = require('fs');
const path = require('path');
const Ajv = require('ajv');
const fetch = require('node-fetch').default;
const { Octokit } = require('@octokit/rest');
const simpleGit = require('simple-git');
const logger = require('./logger');
async function getInquirer() {
try {
const prompts = await import('@inquirer/prompts');
return {
prompt: async questions => {
const answers = {};
for (const question of questions) {
const {
type,
name,
message,
choices,
default: defaultValue,
validate,
filter,
when,
} = question;
if (
when &&
!(typeof when === 'function' ? await when(answers) : when)
) {
continue;
}
let resolvedMessage =
typeof message === 'function' ? await message(answers) : message;
if (!resolvedMessage) {
logger.warn(`Message for prompt ${name} is empty or undefined`);
resolvedMessage = `Enter ${name}`;
}
let resolvedDefault =
typeof defaultValue === 'function'
? await defaultValue(answers)
: defaultValue;
let answer;
switch (type) {
case 'input':
answer = await prompts.input({
message: resolvedMessage,
default: resolvedDefault,
validate: validate
? async input => {
try {
return await validate(input, answers);
} catch (err) {
logger.error(
`Validation error for ${name}: ${err.message}`,
err.stack
);
return `Validation failed: ${err.message}`;
}
}
: undefined,
});
break;
case 'list':
answer = await prompts.select({
message: resolvedMessage,
choices: choices.map(c => ({
value: c,
name: typeof c === 'object' ? c.name || c : c,
})),
default: resolvedDefault,
});
break;
default:
throw new Error(`Unsupported prompt type: ${type}`);
}
if (filter) {
answer = await filter(answer);
}
answers[name] = answer;
}
return answers;
},
};
} catch (err) {
logger.error(`Failed to load @inquirer/prompts: ${err.message}`, err.stack);
throw new Error(`Failed to load inquirer module: ${err.message}`);
}
}
function colorize(text, color) {
return `\x1b[${color}m${text}\x1b[0m`;
}
function mergeConfigWithDefaults(config, defaults) {
return { ...defaults, ...config };
}
const configSchema = {
type: 'object',
properties: {
useNewDir: {
type: 'string',
enum: ['Yes, same as my Package Name', 'Yes, a Custom Name', 'No'],
},
projectDir: { type: 'string' },
useMonorepo: { type: 'string', enum: ['Yes', 'No'] },
monorepoRoot: { type: 'string' },
packageManager: { type: 'string', enum: ['npm', 'pnpm', 'yarn'] },
publishTo: { type: 'string', enum: ['npmjs', 'GitHub Packages', 'Both'] },
access: { type: 'string', enum: ['public', 'private'] },
createGitHubWorkflow: { type: 'string', enum: ['Yes', 'No'] },
createGitHubRepo: { type: 'string', enum: ['Yes', 'No'] },
createGitHubPages: { type: 'string', enum: ['Yes', 'No'] },
ciProvider: {
type: 'string',
enum: ['GitHub Actions', 'GitLab CI', 'CircleCI', 'None'],
},
name: { type: 'string', pattern: '^[a-z0-9-@][a-z0-9-/]*$' },
version: { type: 'string', pattern: '^\\d+\\.\\d+\\.\\d+$' },
moduleType: { type: 'string', enum: ['ES Modules', 'CommonJS'] },
githubUsername: { type: 'string' },
githubRepoName: { type: 'string' },
githubToken: { type: 'string' },
description: { type: 'string' },
authorName: { type: 'string' },
authorEmail: { type: 'string', pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$' },
authorUrl: { type: 'string' },
homepage: { type: 'string' },
keywords: { type: 'array', items: { type: 'string' } },
license: {
type: 'string',
enum: ['MIT', 'ISC', 'Apache-2.0', 'GPL-3.0', 'Unlicense'],
},
useTypeScript: { type: 'string', enum: ['Yes', 'No'] },
useESLint: { type: 'string', enum: ['Yes', 'No'] },
usePrettier: { type: 'string', enum: ['Yes', 'No'] },
testFramework: {
type: 'string',
enum: ['Jest', 'Mocha', 'Vitest', 'None'],
},
dependencies: { type: 'object', additionalProperties: { type: 'string' } },
devDependencies: {
type: 'object',
additionalProperties: { type: 'string' },
},
customScripts: { type: 'object', additionalProperties: { type: 'string' } },
},
required: ['name', 'version', 'license'],
};
function validateConfig(config) {
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(configSchema);
const valid = validate(config);
return {
valid,
errors: validate.errors ? validate.errors.map(err => err.message) : [],
};
}
async function createGitHubRepo(username, repoName, token, access) {
const octokit = new Octokit({ auth: token });
try {
await octokit.repos.createForAuthenticatedUser({
name: repoName,
private: access === 'private',
});
logger.info(`Created GitHub repository: ${username}/${repoName}`);
} catch (err) {
logger.error(`Failed to create GitHub repository: ${err.message}`);
throw err;
}
}
async function fetchNpmPackage(packageName) {
try {
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
return response.ok;
} catch (err) {
logger.warn(`Failed to validate package ${packageName}: ${err.message}`);
return false;
}
}
async function retry(fn, options = { retries: 3, onError: () => {} }) {
let lastError;
for (let i = 0; i < options.retries; i++) {
try {
return await fn();
} catch (err) {
lastError = err;
options.onError(err);
if (i < options.retries - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
throw lastError;
}
function writeFile(filePath, content, description, dryRun) {
if (dryRun) {
logger.info(`Dry run: Would write ${filePath}`);
console.log(colorize(`[Dry Run] Would generate ${filePath}`, '33'));
logger.debug(`${filePath} content:`, content);
return;
}
fs.writeFileSync(filePath, content);
logger.info(`Generated ${filePath}`);
console.log(colorize(`✅ ${description}`, '32'));
}
module.exports = {
getInquirer,
fs,
path,
colorize,
mergeConfigWithDefaults,
logger,
validateConfig,
createGitHubRepo,
fetchNpmPackage,
simpleGit,
retry,
writeFile,
};