@gkalpak/ng-maintain-utils
Version:
A private collection of utilities for developing tools to help maintain (AngularJS-related) GitHub repositories.
256 lines (203 loc) • 11.7 kB
Markdown
# ng-maintain-utils [![Build Status][build-status-image]][build-status]
## Description
A private collection of utilities for developing tools to help maintain (AngularJS-related) GitHub
repositories.
## Usage
**You** should generally not use it. You would use tools built on top of it, for example:
- [ng-cla-check][ng-cla-check]
- [ng-maintain][ng-maintain]
- [ng-pr-merge][ng-pr-merge]
**I** may use it for building other tools (see above). Here is a brief overview of what's in the box:
- **`AbstractCli`:** Can serve as a base-class for creating a `Cli` object that can orchestrate the
execution of some type of work, based on a list of "raw" command-line arguments. It can display
version info (with `--version`), show usage instructions (with `--usage`), outline the commands
that need to be executed to complete the task at hand (with `--instructions`) - a sort of
"dry-run", report the beginning/end of execution, etc.
It exposes the following (public) methods:
- `getPhases(): Phase[]` (**abstract**): This method _must_ be overwritten and return an array of `Phase`
objects (to be used for displaying instructions in the "dry-run" mode).
- `run(rawArgs: string[], doWork?: ({[key: string]: string}) => any): Promise`: Parse the arguments
and take appropriate action (see above).
It also provides a number of "protected" methods, that can be overwritten by sub-classes:
- `_displayExperimentalTool(): void`
- `_displayHeader(headerTmpl: string, input: {[key: string]: string}): void`
- `_displayInstructions(phases: Phase[], input: {[key: string]: string}): void`
- `_displayUsage(usageMessage: string): void`
- `_displayVersionInfo(): void`
- `_getAndValidateInput(rawArgs: string[], argSpecs: ArgSpec[]): Promise<{[key: string]: string}>`
- `_insertEmptyLine<T>(value: T, isRejection?: boolean): T|Promise<T>`
- `_theHappyEnd<T>(value: T): T`
- `_theUnhappyEnd(err: any): Promise<any>`
Requires:
- _config_: `Config`
- **`ArgSpec`/`ArgSpec.Unnamed`:** Represents the specification for a command-line argument. When
applied on a parsed arguments object (such as the ones returned by `Utils#parseArgs()`) it will
extract the corresponding argument's value (either by name (`ArgSpec`) or by position
(`ArgSpec.Unnamed`)), fall back to a default value if necessary, vaidate the value and assign it
to the specified `input` object under the appropriate `key`.
Requires:
- _index_: `number` (`ArgSpec.Unnamed` only)
- _key_: `string`
- _validator_: `(value: any) => boolean`
- _errorCode_: `string`
- _defaultValue?_: `string|boolean`
- **`CleanUper`:** A utility to help coordinate arbitrary tasks with their associated clean-up
process.
The general idea is this:
1. Schedule a task to clean up after `something`.
2. Do `something`.
3. ...possibly do other things here...
4. If anything goes wrong, the app will be able to clean up (or show instructions to the user).
5. When cleaning up after `something` is no longer necessary, unschedule the clean-up task.
Provides the following methods:
- `cleanUp(listOnly: boolean): Promise`: Perform all clean-up tasks (or just list them).
<sub>(Either way, the clean-up task queue is emptied.)</sub>
- `getCleanUpPhase(): Phase`: Returns a clean-up `Phase` object (suitable for `UiUtils#phase()`).
- `hasTasks(): boolean`: Returns whether or not there are any clean-up tasks scheduled.
- `registerTask(description: string, cb: () => Promise): TaskId`: Register a task with the
`CleanUper`. You can use the returned, unique `TaskId` for scheduling/unscheduling the task.
- `schedule(taskId: TaskId): void`: Schedule a clean-up task.
- `unschedule(taskId: TaskId): void`: Schedule (the last instance of) a clean-up task.
- `withTask(taskId: TaskId, fn: () => any): Promise`: Schedule `taskId` and execute `fn` (can also
return a promise). If all goes well, unschedule `taskId`. If an error occurs, leave `taskId`
in the clean-up task queue.
Requires:
- _logger_: `Logger`
- **`Config`:** Creates a `Config` object based on the specified `messages` and `argSpecs` (falling
back to some default values if necessary). It exposes the following properties:
- `argSpecs`: A (possibly empty) array of `ArgSpec` objects.
- `defaults`: A `{[argument: string]: string|number}` map of default values per command-line
argument keys. <sub>(Automatically extracted for `argSpecs`.)</sub>
- `messages`: A (possibly nested) `{[messageKey: string]: string}` map with at least the following
messages:
- `usage`
- `instructionsHeaderTmpl`
- `headerTmpl`
- `errors`:
- `ERROR_unexpected`
- `warnings`:
- `WARN_experimentalTool`
- `versionInfo`: A `{name: string, version: string}` map with values retrieved from the main
module's `package.json` (i.e. the first `package.json` to be found starting from the main file's
directory and moving upwards).
Requires:
- _messages_: `{[messageKey: string]: string}`
- _argSpecs_: `ArgSpec[]`
- **`GitUtils`:** A collection of `Git`-related command-wrappers and utilities. Mainly spawns `Git`
commands in a separate process and (promises to) return the output. Support for commands is added
in an "as-needed" basis. Currently, the available commands/utilities include:
- `abortAm(): Promise`
- `abortRebase(): Promise`
- `checkout(branch: string): Promise`
- `clean(mode?: string = 'interactive'): Promise`
- `countCommitsSince(commit: string): Promise<number>`
- `createBranch(branch: string): Promise`
- `deleteBranch(branch: string, force?: boolean): Promise`
- `diff(commit: string, noColor?: boolean): Promise`
- `diffWithHighlight(commit: string): Promise`
- `diffWithHighlight2(commit: string): Promise`
- `getCommitMessage(commit: string): Promise<string>`
- `getLastCommitMessage(): Promise<string>`
- `log(oneline?: boolean, count?: number, noDecorate?: boolean): Promise`
- `mergePullRequest(prUrl: string): Promise`
- `pull(branch: string, rebase?: boolean): Promise`
- `push(branch: string): Promise`
- `rebase(commit: string|number, interactive?: boolean): Promise`
- `reset(commit: string, hard?: boolean): Promise`
- `setLastCommitMessage(message: string): Promise`
- `updateLastCommitMessage(getNewMessage: (oldMessage: string) => string): Promise`
Requires:
- _cleanUper_: `CleanUper`
- _utils_: `Utils`
- **`GitUtils.DiffHighlighter`:** Can be used to enhance a diff by highlighting areas of interest.
The general implementation is loosely based on the idea of [diff-highlight][diff-highlight],
although the matching heuristics and coloring (among other things) are different.
It exposes the underlying streams via:
- `getInputStream(): stream.PassThrough`
- `getOutputStream(): stream.PassThrough`
Requires:
- _styles?_:
```ts
{
lineRemoved?: (text: string) => string,
lineAdded?: (text: string) => string,
areaRemoved?: (text: string) => string,
areaAdded?: (text: string) => string
}
```
- **`GitUtils.DiffHighlighter2`:** An alternative, API-compatible implementation of
`GitUtils.DiffHighlighter`. The highlighting is more accurate, as it is able to only highlight the
regions that have changed. The main drawback is that it fails to show removed/added empty lines,
because of its dependency on `Git`s `--word-diff=plain` option.
Similar to `GitUtils.DiffHighlighter`, it exposes the underlying streams via:
- `getInputStream(): stream.PassThrough`
- `getOutputStream(): stream.PassThrough`
Requires:
- _styles?_:
```ts
{
lineRemoved?: (text: string) => string,
lineAdded?: (text: string) => string,
areaRemoved?: (text: string) => string,
areaAdded?: (text: string) => string
}
```
- **`Logger`:** A simple helper providing minimal logging utilities. This is mostly used in order to
make it easier to test logging behavior without affecting `console` methods. Provides the
following methods:
- `debug()`: Delegate to `console.debug()`.
- `error()`: Delegate to `console.error()`.
- `info()`: Delegate to `console.info()`.
- `log()`: Delegate to `console.log()`.
- `warn()`: Delegate to `console.warn()`.
- **`Phase`:** A simple wrapper for "phase" entities (with validation). A "phase" is a description
of a unit of work, including an ID, a short description, a list of the tasks involved and an error
message (or code) specific to this "phase".
Requires:
- _id_: `string`
- _description_: `string`
- _instructions?_: `string[]`
- _error?_: `string` <sub>(Can be either an error message or an error code.)</sub>
- **`UiUtils`:** A collection of utilities useful for interacting with the user, including:
- `askQuestion()`: Prompt the user with a question and (promise to) return the answer.
- `askYesOrNoQuestion()`: Prompt the user with a yes-or-no question (e.g. a confirmation) and
(promise to) resolve (for "yes") or reject (for "no").
- `offerToCleanUp()`: Requests confirmation to perform the scheduled clean-up tasks. If the user
turns the offer down, it will just list the pending tasks instead.
- `phase()`: It will report the beginning and end of a "phase" (see `Phase`), do some work and
properly handle possible errors (by means of `reportAndRejectFnGen()`).
- `reportAndRejectFnGen()`: Generates a callback that will report the specified error (plus any
extra error provided during invokation), will offer to clean up (if there are pending tasks and
not configured otherwise) and return a rejection.
Requires:
- _logger_: `Logger`
- _cleanUper_: `CleanUper`
- _errorMessages_: `{[errorCode: string]: string}`
- **`Utils`:** A collection of low-level, specific-purpose utilities, including:
- `asPromised()`: Convert callback-based functions to promise-based.
- `interpolate()`: Replace `{{...}}` placeholders in a string with values.
- `parseArgs()`: Parse command-line arguments (and remove surrounding quotes).
- `resetOutputStyleOnExit()`: Ensure the output style is reset when a process exists.
- `spawnAsPromised()`: Spawn a sub-shell to run a (series of) command(s) with support for piping.
- `waitAsPromised()`: `setTimeout()` wrapped in a promise.
## Testing
The following test-types/modes are available:
- **Code-linting:** `npm run lint`
_Lint JavaScript files using ESLint._
- **Unit tests:** `npm run test-unit`
_Run all the unit tests once. These tests are quick and suitable to be run on every change._
- **E2E tests:** `npm run test-e2e`
_Run all the end-to-end tests once. These test may hit actual API endpoints or perform expensive
I/O operations and are considerably slower than unit tests._
- **All tests:** `npm test` / `npm run test`
_Run all of the above tests (code-linting, unit tests, e2e tests). This command is automatically
run before `npm version` and `npm publish`._
- **"Watch" mode:** `npm run test-watch`
_Watch all files and rerun the unit tests whenever something changes. For performance reasons,
code-linting and e2e tests are omitted._
[build-status]: https://github.com/gkalpak/ng-maintain-utils/actions/workflows/ci.yml
[build-status-image]: https://github.com/gkalpak/ng-maintain-utils/actions/workflows/ci.yml/badge.svg?branch=master&event=push
[diff-highlight]: https://github.com/git/git/blob/master/contrib/diff-highlight/README
[ng-cla-check]: https://www.npmjs.com/package/@gkalpak/ng-cla-check
[ng-maintain]: https://www.npmjs.com/package/@gkalpak/ng-maintain
[ng-pr-merge]: https://www.npmjs.com/package/@gkalpak/ng-pr-merge