From 521a3919c91f187001f5d1b1365197145a8f2ddf Mon Sep 17 00:00:00 2001 From: syi0808 Date: Mon, 7 Oct 2024 22:36:08 +0900 Subject: [PATCH] feat: setup tasks --- README.md | 6 +- src/cli.ts | 65 +++++------- src/registry/npm.ts | 19 ++-- src/registry/registry.ts | 1 + src/tasks/jsr.ts | 4 +- src/tasks/npm.ts | 4 +- src/tasks/prerequisites-check.ts | 68 ++++++++++--- src/tasks/required-conditions-check.ts | 93 ++++++++++++++--- src/tasks/required-missing-information.ts | 118 +++++++++++++++++++--- src/tasks/runner.ts | 70 +++++++++++-- src/types/options.ts | 10 ++ src/utils/package-json.ts | 13 +++ src/utils/version.ts | 7 -- 13 files changed, 367 insertions(+), 111 deletions(-) delete mode 100644 src/utils/version.ts diff --git a/README.md b/README.md index 8186249..325669f 100644 --- a/README.md +++ b/README.md @@ -67,11 +67,11 @@ You can have either package.json or jsr.json. - Notify new version -- Required missing information +- Checking required information - Select SemVer increment or specify new version - - Enter the tag for this pre-release version in npm: (if version is prerelease) + - Select the tag for this pre-release version in npm: (if version is prerelease) -- Prerequisite checks = skip-pre (for safety deploy) +- Prerequisite checks = skip-pre (for deployment reliability) - Checking if remote history is clean... - Checking if the local working tree is clean… - Checking if commits exist since the last release... diff --git a/src/cli.ts b/src/cli.ts index 77170e2..e241f31 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,13 +1,11 @@ import cac from 'cac'; import type { OptionConfig } from 'cac/deno/Option.js'; -import enquirer from 'enquirer'; -import semver, { SemVer } from 'semver'; -import c from 'tinyrainbow'; +import semver from 'semver'; import { pubm } from './index.js'; import type { Options } from './types/options.js'; -import { version } from './utils/version.js'; +import { requiredMissingInformationTasks } from './tasks/required-missing-information.js'; +import { version } from './utils/package-json.js'; -const { prompt } = enquirer; const { RELEASE_TYPES } = semver; interface CliOptions { @@ -16,6 +14,8 @@ interface CliOptions { preview?: boolean; branch: string; anyBranch?: boolean; + preCheck: boolean; + conditionCheck: boolean; cleanup: boolean; tests: boolean; build: boolean; @@ -53,6 +53,16 @@ const options: { description: 'Show tasks without actually executing publish', options: { type: Boolean }, }, + { + rawName: '--no-pre-check', + description: 'Skip prerequisites check task', + options: { type: Boolean }, + }, + { + rawName: '--no-condition-check', + description: 'Skip required conditions check task', + options: { type: Boolean }, + }, { rawName: '--no-cleanup', description: 'Skip cleaning the `node_modules` directory', @@ -122,53 +132,28 @@ function resolveCliOptions(options: CliOptions): Options { skipTests: !options.tests, skipBuild: !options.build, registries: options.registry?.split(','), + skipPrerequisitesCheck: !options.preCheck, + skipConditionsCheck: !options.conditionCheck, }; } cli .command('[version]') - .action(async (versionArg, options: Omit) => { + .action(async (nextVersion, options: Omit) => { console.clear(); - const currentVersion = await version(); - let nextVersion = versionArg; - - if (!nextVersion) { - nextVersion = ( - await prompt<{ version: string }>({ - type: 'select', - choices: RELEASE_TYPES.map((releaseType) => { - const increasedVersion = new SemVer(currentVersion) - .inc(releaseType) - .toString(); - - return { - message: `${releaseType} ${c.dim(increasedVersion)}`, - name: increasedVersion, - }; - }).concat([ - { message: 'Custom version (specify)', name: 'specific' }, - ]), - message: 'Select SemVer increment or specify new version', - name: 'version', - }) - ).version; + const context = { + version: nextVersion, + tag: options.tag, + }; - if (nextVersion === 'specific') { - nextVersion = ( - await prompt<{ version: string }>({ - type: 'input', - message: 'Version', - name: 'version', - }) - ).version; - } - } + await requiredMissingInformationTasks().run(context); await pubm( resolveCliOptions({ ...options, - version: nextVersion, + version: context.version, + tag: context.tag, }), ); }); diff --git a/src/registry/npm.ts b/src/registry/npm.ts index 6f9f88d..3502142 100644 --- a/src/registry/npm.ts +++ b/src/registry/npm.ts @@ -1,18 +1,23 @@ import { exec } from 'tinyexec'; import { Registry } from './registry.js'; -// const NPM_DEFAULT_REGISTRIES = new Set([ -// // https://docs.npmjs.com/cli/v10/using-npm/registry -// 'https://registry.npmjs.org', -// // https://docs.npmjs.com/cli/v10/commands/npm-profile#registry -// 'https://registry.npmjs.org/', -// ]); - export class NpmRegistry extends Registry { + constructor(public packageName: string) { + super(); + } + async npm(args: string[]) { return (await exec('npm', args, { throwOnError: true })).stdout; } + async distTags() { + return Object.keys( + JSON.parse( + await this.npm(['view', this.packageName, 'dist-tags', '--json']), + ), + ); + } + async checkPermission() { return ''; } diff --git a/src/registry/registry.ts b/src/registry/registry.ts index 59ea043..9116538 100644 --- a/src/registry/registry.ts +++ b/src/registry/registry.ts @@ -1,5 +1,6 @@ export abstract class Registry { abstract ping(): Promise; + abstract distTags(): Promise; abstract getVersion(): Promise; abstract publish(): Promise; } diff --git a/src/tasks/jsr.ts b/src/tasks/jsr.ts index aee45f0..5168f9e 100644 --- a/src/tasks/jsr.ts +++ b/src/tasks/jsr.ts @@ -24,7 +24,7 @@ export const jsrPubmTasks: ListrTask = { .prompt(ListrEnquirerPromptAdapter) .run({ type: 'password', - message: 'jsr OTP code: ', + message: 'jsr OTP code', }); if (response === '123123') throw new Error('error'); @@ -35,7 +35,7 @@ export const jsrPubmTasks: ListrTask = { .prompt(ListrEnquirerPromptAdapter) .run({ type: 'password', - message: 'jsr OTP code: ', + message: 'jsr OTP code', }); resolve(); diff --git a/src/tasks/npm.ts b/src/tasks/npm.ts index c51048e..5183e9c 100644 --- a/src/tasks/npm.ts +++ b/src/tasks/npm.ts @@ -24,7 +24,7 @@ export const npmPubmTasks: ListrTask = { .prompt(ListrEnquirerPromptAdapter) .run({ type: 'password', - message: 'npm OTP code: ', + message: 'npm OTP code', }); if (response === '123123') throw new Error('asd'); @@ -35,7 +35,7 @@ export const npmPubmTasks: ListrTask = { .prompt(ListrEnquirerPromptAdapter) .run({ type: 'password', - message: 'npm OTP code: ', + message: 'npm OTP code', }); resolve(); diff --git a/src/tasks/prerequisites-check.ts b/src/tasks/prerequisites-check.ts index bc21a74..e27fbfb 100644 --- a/src/tasks/prerequisites-check.ts +++ b/src/tasks/prerequisites-check.ts @@ -1,17 +1,57 @@ -import { type ListrTask, delay } from 'listr2'; +import { Listr, type ListrTask, delay } from 'listr2'; import type { Ctx } from './runner.js'; -export const prerequisitesCheckTask: ListrTask = { - title: 'Prerequisites check (for deployment reliability)', - task: (_, parentTask) => - parentTask.newListr([ - { - title: 'prerequisite 1', - task: async (_, task) => { - task.output = 'All good'; - await delay(1000); +export const prerequisitesCheckTask: ( + options?: Omit, 'title' | 'task'>, +) => Listr = (options) => + new Listr({ + ...options, + exitOnError: true, + title: 'Prerequisites check (for deployment reliability)', + task: (_, parentTask) => + parentTask.newListr([ + { + title: 'Checking if remote history is clean', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, }, - exitOnError: true, - }, - ]), -}; + { + title: 'Checking if the local working tree is clean', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + title: 'Checking if commits exist since the last release', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + title: 'Confirming new files and new dependencies', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + title: 'Checking if the package has never been deployed', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + skip: () => true, + title: 'Checking package name availability', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + ]), + }); diff --git a/src/tasks/required-conditions-check.ts b/src/tasks/required-conditions-check.ts index 23e79e4..f0ac98f 100644 --- a/src/tasks/required-conditions-check.ts +++ b/src/tasks/required-conditions-check.ts @@ -1,17 +1,82 @@ -import { type ListrTask, delay } from 'listr2'; +import { Listr, type ListrTask, delay } from 'listr2'; import type { Ctx } from './runner.js'; -export const requiredConditionsCheckTask: ListrTask = { - title: 'Required conditions check (for pubm tasks)', - task: (_, parentTask) => - parentTask.newListr([ - { - title: 'prerequisite 1 ', - task: async () => { - // console.log('All good'); - await delay(1000); +export const requiredConditionsCheckTask: ( + options?: Omit, 'title' | 'task'>, +) => Listr = (options) => + new Listr({ + ...options, + title: 'Required conditions check (for pubm tasks)', + task: (_, parentTask) => + parentTask.newListr( + [ + { + title: 'Ping registries', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + title: 'Checking if test and build scripts exist', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + title: 'Checking package manager version', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + skip: () => true, + title: 'Verifying user authentication', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + title: 'Checking git version', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + title: 'Checking git remote', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + title: 'Checking git tag existence', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + title: 'Verifying current branch is a release branch', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + title: 'Checking if registry cli are installed', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + ], + { + concurrent: true, }, - exitOnError: true, - }, - ]), -}; + ), + }); diff --git a/src/tasks/required-missing-information.ts b/src/tasks/required-missing-information.ts index 0335792..e55a115 100644 --- a/src/tasks/required-missing-information.ts +++ b/src/tasks/required-missing-information.ts @@ -1,17 +1,103 @@ -import { type ListrTask, delay } from 'listr2'; -import type { Ctx } from './runner.js'; - -export const requiredMissingInformationTasks: ListrTask = { - title: 'Required missing information', - task: (_, parentTask) => - parentTask.newListr([ - { - title: 'prerequisite 1 ', - task: async () => { - // console.log('All good'); - await delay(1000); +import { Listr, type ListrTask } from 'listr2'; +import c from 'tinyrainbow'; +import semver from 'semver'; +import { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer'; +import { packageName, version } from '../utils/package-json.js'; +import { NpmRegistry } from '../registry/npm.js'; + +const { RELEASE_TYPES, SemVer, prerelease } = semver; + +interface Ctx { + version?: string; + tag: string; +} + +export const requiredMissingInformationTasks: ( + options?: Omit, 'title' | 'task'>, +) => Listr = (options) => + new Listr({ + ...options, + title: 'Checking required information', + task: (_, parentTask) => + parentTask.newListr([ + { + title: 'Checking version information', + skip: (ctx) => !!ctx.version, + task: async (ctx, task) => { + const currentVersion = await version(); + + let nextVersion = await task + .prompt(ListrEnquirerPromptAdapter) + .run({ + type: 'select', + message: 'Select SemVer increment or specify new version', + choices: RELEASE_TYPES.map((releaseType) => { + const increasedVersion = new SemVer(currentVersion) + .inc(releaseType) + .toString(); + + return { + message: `${releaseType} ${c.dim(increasedVersion)}`, + name: increasedVersion, + }; + }).concat([ + { message: 'Custom version (specify)', name: 'specify' }, + ]), + name: 'version', + }); + + if (nextVersion === 'specify') { + nextVersion = await task + .prompt(ListrEnquirerPromptAdapter) + .run({ + type: 'input', + message: 'Version', + name: 'version', + }); + } + + ctx.version = nextVersion; + }, + exitOnError: true, + }, + { + title: 'Checking tag information', + skip: (ctx) => !prerelease(`${ctx.version}`), + task: async (ctx, task) => { + const npm = new NpmRegistry(await packageName()); + const distTags = [...(await npm.distTags())].filter( + (tag) => tag !== 'latest', + ); + + if (distTags.length <= 0) distTags.push('next'); + + let tag = await task + .prompt(ListrEnquirerPromptAdapter) + .run({ + type: 'select', + message: 'Select the tag for this pre-release version in npm', + choices: distTags + .map((distTag) => ({ + message: distTag, + name: distTag, + })) + .concat([ + { message: 'Custom version (specify)', name: 'specify' }, + ]), + name: 'tag', + }); + + if (tag === 'specify') { + tag = await task.prompt(ListrEnquirerPromptAdapter).run({ + type: 'input', + message: 'Tag', + name: 'tag', + }); + } + + ctx.tag = tag; + }, + exitOnError: true, }, - exitOnError: true, - }, - ]), -}; + ]), + }); diff --git a/src/tasks/runner.ts b/src/tasks/runner.ts index 37ab0ee..db97348 100644 --- a/src/tasks/runner.ts +++ b/src/tasks/runner.ts @@ -1,9 +1,10 @@ -import { Listr } from 'listr2'; +import { delay, Listr } from 'listr2'; import type { ResolvedOptions } from '../types/options.js'; import { jsrPubmTasks } from './jsr.js'; import { npmPubmTasks } from './npm.js'; import { prerequisitesCheckTask } from './prerequisites-check.js'; import { requiredConditionsCheckTask } from './required-conditions-check.js'; +import { packageName } from '../utils/package-json.js'; export interface Ctx extends ResolvedOptions { progressingPrompt?: Promise; @@ -13,13 +14,70 @@ export async function run(options: ResolvedOptions) { const ctx = { ...options }; try { - await new Listr(prerequisitesCheckTask, {}).run(ctx); - - await new Listr([npmPubmTasks, jsrPubmTasks], { - exitOnError: true, - concurrent: true, + await prerequisitesCheckTask({ skip: options.skipPrerequisitesCheck }).run( ctx, + ); + + await requiredConditionsCheckTask({ + skip: options.skipConditionsCheck, }).run(ctx); + + await new Listr([ + { + skip: options.skipTests, + title: 'Running tests', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + skip: options.skipBuild, + title: 'Building the project', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + title: 'Bumping version', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + skip: options.skipPublish, + title: 'Publishing', + task: (_, parentTask) => + parentTask.newListr([npmPubmTasks, jsrPubmTasks], { + exitOnError: true, + concurrent: true, + ctx, + }), + }, + { + title: 'Pushing tags to GitHub', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + { + skip: options.skipReleaseDraft, + title: 'Creating release draft on GitHub', + task: async (_, task) => { + task.output = 'All good'; + await delay(1000); + }, + }, + ]).run(ctx); + + console.log( + ` +🚀 Successfully published ${await packageName()} v${ctx.version} 🚀 + `, + ); } catch (e) { console.error(e); } diff --git a/src/types/options.ts b/src/types/options.ts index 0b6dfd9..25ce40d 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -50,6 +50,16 @@ export interface Options { * @default false */ skipReleaseDraft?: boolean; + /** + * @description Skip prerequisites check task + * @default false + */ + skipPrerequisitesCheck?: boolean; + /** + * @description Skip required conditions check task + * @default false + */ + skipConditionsCheck?: boolean; /** * @description Skip both cleanup and tests * @default false diff --git a/src/utils/package-json.ts b/src/utils/package-json.ts index d20faf7..1feba85 100644 --- a/src/utils/package-json.ts +++ b/src/utils/package-json.ts @@ -5,6 +5,7 @@ import process from 'node:process'; export type Engine = 'node' | 'git' | 'npm' | 'pnpm' | 'yarn'; interface PackageJson { + name: string; version: string; engine: Record; } @@ -41,3 +42,15 @@ export async function getPackageJson({ cwd = process.cwd() } = {}) { throw new Error('root package.json is not json format.'); } } + +export async function version({ cwd = process.cwd() } = {}) { + const { version } = await getPackageJson({ cwd }); + + return version; +} + +export async function packageName({ cwd = process.cwd() } = {}) { + const { name } = await getPackageJson({ cwd }); + + return name; +} diff --git a/src/utils/version.ts b/src/utils/version.ts deleted file mode 100644 index 2152677..0000000 --- a/src/utils/version.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { getPackageJson } from './package-json.js'; - -export async function version({ cwd = process.cwd() } = {}) { - const { version } = await getPackageJson({ cwd }); - - return version; -}