Skip to content

Commit

Permalink
feat(run-if-supported): create a new cli package (#80)
Browse files Browse the repository at this point in the history
* chore(run-if-supported): create directory for `@sounisi5011/run-if-supported` cli

* chore(run-if-supported): add the required dependencies

* chore(run-if-supported): create a type definition file for the `npm-install-checks` package

* feat(run-if-supported): add script files for the CLI

* chore(run-if-supported): remove unused dependencies

* test(run-if-supported): add basic tests

* fix(run-if-supported): write errors to stderr

* test(run-if-supported): fix some tests

* fix(run-if-supported): quote arguments that contain whitespace characters

* test(run-if-supported): add tests for the parser of CLI arguments

* test(run-if-supported): add tests for the `isNotSupported()` function

* refactor(run-if-supported): reduce Cognitive Complexity in `getCliData()` function

    Code Climate reported:
    + Function `getCliData` has a Cognitive Complexity of 20 (exceeds 5 allowed). Consider refactoring.

* refactor(run-if-supported): reduce Cognitive Complexity in `getBinName()` function

    Code Climate reported:
    + Function `getBinName` has a Cognitive Complexity of 11 (exceeds 5 allowed). Consider refactoring.

* refactor(run-if-supported): reduce lines in `createHelpText()` function

    Code Climate reported:
    + Function `createHelpText` has 32 lines of code (exceeds 25 allowed). Consider refactoring.

* refactor(run-if-supported): reduce lines and Cognitive Complexity in `main()` function

    Code Climate reported:
    + Function `main` has a Cognitive Complexity of 16 (exceeds 5 allowed). Consider refactoring.
    + Function `main` has 30 lines of code (exceeds 25 allowed). Consider refactoring.

* refactor(run-if-supported): reduce Cognitive Complexity in `main()` function

    Code Climate reported:
    + Function `main` has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring.

* refactor(run-if-supported): reduce lines and Cognitive Complexity in `parseOptions()` function

    Code Climate reported:
    + Function `parseOptions` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.
    + Function `parseOptions` has 36 lines of code (exceeds 25 allowed). Consider refactoring.

* refactor(run-if-supported): reduce lines in `parseOptions()` function

    Code Climate reported:
    + Function `parseOptions` has 30 lines of code (exceeds 25 allowed). Consider refactoring.

* refactor(run-if-supported): simplify the functions `parseOptions`, `processOption`, `parseLongOption`, and `parseShortOption`

* refactor(run-if-supported): reduce Cognitive Complexity in `createRequiredPlatformText()` function

    Code Climate reported:
    + Function `createRequiredPlatformText` has a Cognitive Complexity of 8 (exceeds 5 allowed). Consider refactoring.

* refactor(run-if-supported): reduce Cognitive Complexity in `isNotSupported()` function

    Code Climate reported:
    + Function `isNotSupported` has a Cognitive Complexity of 11 (exceeds 5 allowed). Consider refactoring.

* docs(run-if-supported): add `README.md`

* docs(run-if-supported): add keywords to `package.json`

    + `cpu`
    + `exec`, `execute` and `execution`
    + `*s`

* build(run-if-supported): exclude `*.d.ts` files from the package

    This package is a CLI, so it should not require type definition files.

* ci(run-if-supported): add custom publish scripts
  • Loading branch information
sounisi5011 authored May 25, 2021
1 parent 26bb403 commit 696bf0f
Show file tree
Hide file tree
Showing 26 changed files with 1,449 additions and 18 deletions.
5 changes: 4 additions & 1 deletion .eslintrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ overrides:
parserOptions:
sourceType: module
project:
- ./{packages{,/ts-type-utils},actions}/*{,/tests{,/helpers},/test-d}/tsconfig.json
- ./packages/*{,/tests{,/helpers},/test-d}/tsconfig.json
- ./packages/cli/*{,/tests{,/helpers},/test-d}/tsconfig.json
- ./packages/ts-type-utils/*{,/tests{,/helpers},/test-d}/tsconfig.json
- ./actions/*{,/tests{,/helpers},/test-d}/tsconfig.json
settings:
node:
# see https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/docs/rules/shebang.md
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/run-if-supported/.github/workflows/publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash

# https://qiita.com/yudoufu/items/48cb6fb71e5b498b2532#comment-87e291b98f4cabf77138
readonly DIR_PATH="$(cd "$(dirname "${BASH_SOURCE:-${(%):-%N}}")"; pwd)"

outputs_tag_name="${outputs_tag_name}" \
node "${DIR_PATH}/../../scripts/publish-convert-readme.js"

# see https://stackoverflow.com/a/62675843/4907315
pnpm publish --access=public --no-git-checks
105 changes: 105 additions & 0 deletions packages/cli/run-if-supported/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# @sounisi5011/run-if-supported

<!-- [![Go to the latest release page on npm](https://img.shields.io/npm/v/@sounisi5011/run-if-supported.svg)](https://www.npmjs.com/package/@sounisi5011/run-if-supported) -->
<!-- ![Supported Node.js version: ^12.17.x || 14.x || 15.x || 16.x](https://img.shields.io/node/v/@sounisi5011/run-if-supported) -->
[![Tested with Jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest)
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
<!-- [![Minified Bundle Size Details](https://img.shields.io/bundlephobia/min/@sounisi5011/run-if-supported)](https://bundlephobia.com/result?p=@sounisi5011/run-if-supported) -->
<!-- [![Install Size Details](https://packagephobia.com/badge?p=@sounisi5011/run-if-supported)](https://packagephobia.com/result?p=@sounisi5011/run-if-supported) -->
[![Dependencies Status](https://status.david-dm.org/gh/sounisi5011/npm-packages.svg?path=packages%2Fcli%2Frun-if-supported)](https://david-dm.org/sounisi5011/npm-packages?path=packages/cli/run-if-supported)
[![Build Status](https://github.com/sounisi5011/npm-packages/actions/workflows/ci.yaml/badge.svg)](https://github.com/sounisi5011/npm-packages/actions/workflows/ci.yaml)
[![Maintainability Status](https://api.codeclimate.com/v1/badges/26495b68302f7ff963c3/maintainability)](https://codeclimate.com/github/sounisi5011/npm-packages/maintainability)

Execute the command only if you are running on a supported version of Node and platform.
By using this CLI, you can run tests only on the supported node versions and platforms, for example, when testing in multiple environments with CI.

## Installation

```sh
npm install --save-dev @sounisi5011/run-if-supported
```

```sh
yarn add @sounisi5011/run-if-supported --dev
```

```sh
pnpm add --save-dev @sounisi5011/run-if-supported
```

## Usage

For example, if you want to run the command `jest`:

```console
$ run-if-supported jest
# ...
# jest's result
# ...
```

Add the `--verbose` option if you want to display the executed command and the reason why it was skipped.

```console
$ run-if-supported --verbose jest
> $ jest
# ...
# jest's result
# ...
```

```console
$ run-if-supported --verbose jest
Skipped command execution. ...
```

If you want to show only the reason for skipping, add the `--print-skip-message` option.

```console
$ run-if-supported --print-skip-message jest
# ...
# jest's result
# ...
```

```console
$ run-if-supported --print-skip-message jest
Skipped command execution. ...
```

For more information, use the `--help` option to see how to use it, or refer to the [`tests/cli.ts` file](./tests/cli.ts).

## Define supported versions

[npm-install-checks]: https://github.com/npm/npm-install-checks

To define the supported Node.js versions, use the [`engines.node` field](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#engines) of `package.json`.

```json
{
"engines": {
"node": "12.x || 14.x || 16.x"
}
}
```

This CLI uses the [same logic as the npm CLI][npm-install-checks] to check the supported versions.

## Define supported platforms

To define the supported platforms, use the [`os` field](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#os) and [`cpu` field](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#cpu) of `package.json`.

```json
{
"os": [
"win32",
"darwin",
"linux"
],
"cpu": [
"any"
]
}
```

This CLI uses the [same logic as the npm CLI][npm-install-checks] to check the supported platforms.
15 changes: 15 additions & 0 deletions packages/cli/run-if-supported/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
preset: 'ts-jest',
coverageDirectory: 'coverage',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tests/tsconfig.json',
},
},
testEnvironment: 'node',
testMatch: ['<rootDir>/tests/**/*.ts'],
testPathIgnorePatterns: [
'<rootDir>/tests/fixtures/',
'<rootDir>/tests/helpers/',
],
};
75 changes: 75 additions & 0 deletions packages/cli/run-if-supported/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"name": "@sounisi5011/run-if-supported",
"version": "0.0.0",
"description": "Execute the command only if you are running on a supported version of Node and platform",
"keywords": [
"ci",
"cli",
"command",
"commandline",
"cpu",
"exec",
"execute",
"execution",
"node",
"os",
"platform",
"platforms",
"run",
"support",
"supported",
"supports",
"tool",
"tools",
"version",
"versions"
],
"homepage": "https://github.com/sounisi5011/npm-packages/tree/main/packages/cli/run-if-supported#readme",
"bugs": {
"url": "https://github.com/sounisi5011/npm-packages/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sounisi5011/npm-packages.git",
"directory": "packages/cli/run-if-supported"
},
"license": "MIT",
"author": "sounisi5011",
"type": "commonjs",
"bin": {
"run-if-supported": "./dist/index.js"
},
"directories": {
"lib": "./src/",
"test": "./tests/"
},
"files": [
"dist/",
"!*.d.ts",
"!*.tsbuildinfo"
],
"scripts": {
"build": "tsc -p ./src/",
"lint:tsc": "run-p lint:tsc:*",
"lint:tsc:src": "tsc -p ./src/ --noEmit",
"lint:tsc:test": "tsc -p ./tests/ --noEmit",
"test": "jest"
},
"dependencies": {
"command-join": "^3.0.0",
"cross-spawn": "^7.0.3",
"npm-install-checks": "^4.0.0",
"parse-json": "^5.2.0",
"pkg-up": "^3.1.0"
},
"devDependencies": {
"@types/cross-spawn": "6.0.2",
"@types/node": "*",
"@types/parse-json": "^4.0.0",
"execa": "5.0.0",
"ultra-runner": "3.10.5"
},
"engines": {
"node": "^12.17.x || 14.x || 15.x || 16.x"
}
}
87 changes: 87 additions & 0 deletions packages/cli/run-if-supported/scripts/publish-convert-readme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const fs = require('fs');
const path = require('path');

const getPath = pathname => path.resolve(__dirname, pathname);

const pkgPath = getPath('../package.json');
const readmePath = getPath('../README.md');

const pkg = require(pkgPath);

/**
* @param {string} name
* @returns {string|undefined}
*/
function lookupEnv(name) {
for (const [envName, value] of Object.entries(process.env)) {
if (envName.toLowerCase() === name.toLowerCase()) {
return value;
}
}
return undefined;
}

const repoURL = pkg.repository
&& (typeof pkg.repository === 'string' ? pkg.repository : pkg.repository.url)
.replace(/^git\+/, '')
.replace(/\.git$/, '');
const directory = pkg.repository && typeof pkg.repository === 'object'
? pkg.repository.directory.replace(/\/*$/, '/')
: '';
const tagName = lookupEnv('outputs_tag_name');
const rootURL = repoURL ? `${repoURL}/tree/${tagName || 'main'}/${directory}` : '';

/**
* @param {string} url
* @returns {string}
*/
function replaceURL(url) {
if (pkg.version) {
if (url.startsWith('https://img.shields.io/bundlephobia/') && !/\/\d+(?:\.\d+){2}$/.test(url)) {
return `${url}/${pkg.version}`;
}
if (
url.startsWith('https://bundlephobia.com/result?p=')
|| url.startsWith('https://packagephobia.com/badge?p=')
|| url.startsWith('https://packagephobia.com/result?p=')
) {
return url.replace(/(?:@\d+(?:\.\d+){2})?$/, `@${pkg.version}`);
}
}
if (rootURL) {
if (url.startsWith('./')) {
return rootURL + url.replace(/^\.\//, '');
}
}
if (pkg.license) {
if (url.startsWith('https://img.shields.io/npm/l/')) {
return `https://img.shields.io/static/v1?label=license&message=${encodeURIComponent(pkg.license)}&color=green`;
}
}
if (pkg.engines && pkg.engines.node && url.startsWith('https://img.shields.io/node/v/')) {
return `https://img.shields.io/static/v1?label=node&message=${
encodeURIComponent(pkg.engines.node)
}&color=brightgreen`;
}
return url;
}

const readmeText = fs.readFileSync(readmePath, 'utf8');

const updatedReadmeText = readmeText
.replace(/(?<=\()(?:https?:\/\/|\.{1,2}\/)[^)\s]+/g, url => {
const newUrl = replaceURL(url);
if (newUrl !== url) {
console.log(`replace "${url}"\n to "${newUrl}"`);
}
return newUrl;
})
.replace(/(?<=\]: *)(?:https?:\/\/|\.{1,2}\/)[^\s]+/g, url => {
const newUrl = replaceURL(url);
if (newUrl !== url) {
console.log(`replace "${url}"\n to "${newUrl}"`);
}
return newUrl;
});

fs.writeFileSync(readmePath, updatedReadmeText);
17 changes: 17 additions & 0 deletions packages/cli/run-if-supported/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env node

import { argv, cwd as getCwd, versions } from 'process';

import { main } from './main';
import { spawnAsync } from './spawn';

main({
cwd: getCwd(),
entryFilepath: __filename,
argv: argv.slice(2),
nodeVersion: versions.node,
spawnAsync,
}).catch(error => {
process.exitCode = 1;
console.error(error);
});
68 changes: 68 additions & 0 deletions packages/cli/run-if-supported/src/is-supported.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { checkEngine, checkPlatform } from 'npm-install-checks';

import { isRecordLike, isString } from './utils';

function readProp<T>(
obj: unknown,
prop: string,
validate: (value: unknown) => value is T,
): T | null {
if (isRecordLike(obj)) {
const value = obj[prop];
if (validate(value)) return value;
}
return null;
}

function validateError<T>(error: unknown, fn: (error: Error & Record<PropertyKey, unknown>) => T): T {
if (error instanceof Error && isRecordLike(error)) return fn(error);
throw error;
}

function createRequiredPlatformText(error: Error & Record<PropertyKey, unknown>): string {
if (!isRecordLike(error['current']) || !isRecordLike(error['required'])) return '';
return Object.entries(error['current'])
.flatMap(([prop, currentPlatform]): string[] => {
if (!isString(currentPlatform)) return [];
const requiredPlatformList = (
readProp(error['required'], prop, Array.isArray)
?? [readProp(error['required'], prop, isString)]
).filter(isString);
if (requiredPlatformList.length < 1) return [];
return [
`${prop}:`,
` current: ${currentPlatform}`,
` required:`,
...requiredPlatformList.map(platform => ` - ${/\s|^$/.test(platform) ? `"${platform}"` : platform}`),
];
})
.map(line => ` ${line}`)
.join('\n');
}

export function isNotSupported(
pkg: Record<string, unknown>,
nodeVersion: string,
): string | false {
try {
checkEngine(pkg, null, nodeVersion);
} catch (error: unknown) {
return validateError(error, error => {
if (error['code'] !== 'EBADENGINE') throw error;
const nodeRange = readProp(error['required'], 'node', isString);
return `Node ${nodeVersion} is not included in supported range${nodeRange ? `: ${nodeRange}` : ''}`;
});
}
try {
checkPlatform(pkg);
} catch (error: unknown) {
return validateError(error, error => {
if (error['code'] !== 'EBADPLATFORM') throw error;
const requiredPlatform = createRequiredPlatformText(error);
return `Current platform is not included in supported list${
requiredPlatform ? `:\n${requiredPlatform}` : ''
}`;
});
}
return false;
}
Loading

0 comments on commit 696bf0f

Please sign in to comment.