Repository: karen-irc/option-t
Pull request #1314 (2022-06-16 21:28:25)

Use cli flags instead of envvar for utility scripts
We'd like to use `parseArgs` in `node:util` but it requires Node.js v18.3.
Instead, we use [`@pkgjs/parseargs`](https://github.com/pkgjs/parseargs) as polyfill.


Patch:
diff --git a/Makefile b/Makefile
index 9699a612..3d038b8d 100644
--- a/Makefile
+++ b/Makefile
@@ -96,8 +96,7 @@ build_mjs_cp_dts_to_esm: build_mjs_create_tmp_mjs clean_dist
 
 .PHONY: build_mjs_rename_js_to_mjs
 build_mjs_rename_js_to_mjs: build_mjs_create_tmp_mjs
-	TARGET_DIR=$(TMP_MJS_DIR) \
-    $(NODE_BIN) $(CURDIR)/tools/extension_renamer.mjs
+	$(NODE_BIN) $(CURDIR)/tools/extension_renamer.mjs --target-dir $(TMP_MJS_DIR)
 
 .PHONY: build_mjs_create_tmp_mjs
 build_mjs_create_tmp_mjs: build_mjs_create_tmp_mjs_call_tsc build_mjs_create_tmp_mjs_call_babel build_mjs_create_tmp_mjs_cal_cpx
@@ -141,9 +140,7 @@ cp_readme: clean_dist
 
 .PHONY: generate_manifest
 generate_manifest: clean_dist
-	INPUT_MANIFEST_PATH=$(CURDIR)/package.json \
-    OUTDIR=$(DIST_DIR) \
-    $(NODE_BIN) $(CURDIR)/tools/package_json_rewriter/main.mjs
+	$(NODE_BIN) $(CURDIR)/tools/package_json_rewriter/main.mjs --input-manifest-path $(CURDIR)/package.json --destination $(DIST_DIR)
 
 
 ###########################
@@ -182,7 +179,7 @@ test_distribution_contain_all: build
 
 .PHONY: run_test_distribution_contain_all
 run_test_distribution_contain_all:
-	OUTDIR=$(DIST_DIR) $(NODE_BIN) $(CURDIR)/tools/test_package_contains_expected_all.mjs
+	$(NODE_BIN) $(CURDIR)/tools/test_package_contains_expected_all.mjs --target $(DIST_DIR)
 
 .PHONY: test_esmodule_path_rewrite
 test_esmodule_path_rewrite: build
@@ -190,7 +187,7 @@ test_esmodule_path_rewrite: build
 
 .PHONY: run_test_esmodule_path_rewrite
 run_test_esmodule_path_rewrite:
-	OUTDIR=$(DIST_DIR) $(NODE_BIN) $(CURDIR)/tools/test_esmodule_path_rewrite.mjs
+	$(NODE_BIN) $(CURDIR)/tools/test_esmodule_path_rewrite.mjs --target $(DIST_DIR)
 
 .PHONY: test_package_install
 test_package_install: build __run_install_package
@@ -227,9 +224,7 @@ eslint_fix: ## Apply ESLint's `--fix` mode
 
 .PHONY: generate_import_path_list_md
 generate_import_path_list_md: ## Generate all public import paths to docs/import_path.md
-	OUT_DIR=${DOCS_DIR} \
-    SRC_DIR=${SRC_DIR} \
-    $(NODE_BIN) $(CURDIR)/tools/generate_import_path_list_markdown.mjs
+	$(NODE_BIN) $(CURDIR)/tools/generate_import_path_list_markdown.mjs --source ${SRC_DIR} --destination ${DOCS_DIR}
 
 TARGETS_SHOULD_BE_RESET_AFTER_TEST_TO_INSTALL := \
   package.json \
diff --git a/package.json b/package.json
index 292eb709..68630330 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
     "@babel/cli": "^7.17.10",
     "@babel/core": "^7.18.2",
     "@babel/preset-env": "^7.18.2",
+    "@pkgjs/parseargs": "^0.9.0",
     "@typescript-eslint/eslint-plugin": "^5.27.1",
     "@typescript-eslint/parser": "^5.28.0",
     "ava": "^4.3.0",
diff --git a/tools/cp_files.mjs b/tools/cp_files.mjs
index 33b5a43d..6ea8e509 100644
--- a/tools/cp_files.mjs
+++ b/tools/cp_files.mjs
@@ -1,8 +1,9 @@
+import * as assert from 'node:assert/strict';
 import fs from 'node:fs/promises';
 import path from 'node:path';
+import { parseArgs } from '@pkgjs/parseargs';
 
 import { getAllGlobMatchedFiles } from './glob.mjs';
-import { parseArgs } from './parse_argv.mjs';
 
 async function createSourceToDestinationMapList(baseDir, sourceList, destinationDir) {
     const normalizedDest = path.normalize(destinationDir);
@@ -51,37 +52,59 @@ async function copyFile(
     return copying;
 }
 
-const CLI_FLAG_VERBOSE = '--verbose';
-const CLI_FLAG_DEBUG = '--debug';
-const CLI_FLAG_BASE_DIR = '--basedir';
-const CLI_FLAG_SOURCE = '--source';
-const CLI_FLAG_DESTINATION = '--destination';
+function parseCliOptions() {
+    const options = {
+        verbose: {
+            type: 'boolean',
+        },
+        debug: {
+            type: 'boolean',
+        },
+        basedir: {
+            type: 'string',
+        },
+        source: {
+            type: 'string',
+        },
+        destination: {
+            type: 'string',
+        },
+    };
+
+    const { values } = parseArgs({
+        options,
+        strict: true,
+    });
 
-(async function main() {
-    const argSet = new Set(process.argv);
-    // eslint-disable-next-line no-magic-numbers
-    const argv = parseArgs(process.argv.slice(2));
+    const isVerbose = !!values.verbose;
+    const isDebug = !!values.debug;
 
-    const isVerbose = argSet.has(CLI_FLAG_VERBOSE);
-    const isDebug = argSet.has(CLI_FLAG_DEBUG);
-    if (isDebug) {
-        console.log(`process.argv: ${JSON.stringify(argv)}`);
-    }
+    const baseDir = values.basedir;
+    assert.ok(!!baseDir, 'no baseDir');
 
-    const baseDir = argv.get(CLI_FLAG_BASE_DIR);
-    if (!baseDir) {
-        throw new Error('no baseDir');
-    }
+    const source = values.source;
+    assert.ok(!!source, 'no source');
 
-    const source = argv.get(CLI_FLAG_SOURCE);
-    if (!source) {
-        throw new Error('no source');
-    }
+    const destinationDir = values.destination;
+    assert.ok(!!destinationDir, 'no destinationDir');
 
-    const destinationDir = argv.get(CLI_FLAG_DESTINATION);
-    if (!destinationDir) {
-        throw new Error('no target');
-    }
+    return {
+        isVerbose,
+        isDebug,
+        baseDir,
+        source,
+        destinationDir,
+    };
+}
+
+(async function main() {
+    const {
+        isVerbose,
+        isDebug,
+        baseDir,
+        source,
+        destinationDir,
+    } = parseCliOptions();
 
     const baseDirFullPath = path.resolve(process.cwd(), baseDir);
     const sourceList = await getAllGlobMatchedFiles(baseDirFullPath, source);
diff --git a/tools/extension_renamer.mjs b/tools/extension_renamer.mjs
index 9e2a409c..33f77b6f 100644
--- a/tools/extension_renamer.mjs
+++ b/tools/extension_renamer.mjs
@@ -2,6 +2,7 @@ import * as assert from 'node:assert/strict';
 import * as fs from 'node:fs/promises';
 import * as path from 'node:path';
 import { fileURLToPath } from 'node:url';
+import { parseArgs } from '@pkgjs/parseargs';
 
 const FROM_EXTENSION = 'js';
 const TO_EXTENSION = 'mjs';
@@ -55,9 +56,32 @@ function bulkRename(filePathList, targetPattern, replaceValue) {
     return Promise.all(result);
 }
 
+function parseCliOptions() {
+    const CLI_OPTION_TARGET_DIR = 'target-dir';
+
+    const options = {
+        [CLI_OPTION_TARGET_DIR]: {
+            type: 'string',
+        },
+    };
+
+    const { values } = parseArgs({
+        options,
+        strict: true,
+    });
+
+    const targetDir = values[CLI_OPTION_TARGET_DIR];
+    assert.ok(!!targetDir, 'no targetDir');
+
+    return {
+        targetDir,
+    };
+}
+
 (async function main() {
-    const TARGET_DIR = process.env.TARGET_DIR;
-    assert.ok(typeof TARGET_DIR === 'string', 'TARGET_DIR should be string');
+    const {
+        targetDir: TARGET_DIR,
+    } = parseCliOptions();
 
     const cwd = process.cwd();
     const subrootDir = path.resolve(cwd, TARGET_DIR);
diff --git a/tools/generate_import_path_list_markdown.mjs b/tools/generate_import_path_list_markdown.mjs
index cfa23c8d..47b85452 100644
--- a/tools/generate_import_path_list_markdown.mjs
+++ b/tools/generate_import_path_list_markdown.mjs
@@ -2,6 +2,7 @@ import * as assert from 'node:assert/strict';
 import * as fs from 'node:fs/promises';
 import * as path from 'node:path';
 import { fileURLToPath } from 'node:url';
+import { parseArgs } from '@pkgjs/parseargs';
 
 import {
     generateExposedPathSequence,
@@ -117,12 +118,38 @@ ${str}
 `;
 }
 
-(async function main() {
-    const OUT_DIR = process.env.OUT_DIR;
-    assert.strictEqual(typeof OUT_DIR, 'string', '$OUTDIR envvar should be string');
+function parseCliOptions() {
+    const options = {
+        source: {
+            type: 'string',
+        },
+        destination: {
+            type: 'string',
+        },
+    };
+
+    const { values } = parseArgs({
+        options,
+        strict: true,
+    });
+
+    const sourceDir = values.source;
+    assert.ok(!!sourceDir, 'no source');
+
+    const destinationDir = values.destination;
+    assert.ok(!!destinationDir, 'no destinationDir');
+
+    return {
+        sourceDir,
+        destinationDir,
+    };
+}
 
-    const SRC_DIR = process.env.SRC_DIR;
-    assert.strictEqual(typeof SRC_DIR, 'string', '$SRC_DIR envvar should be string');
+(async function main() {
+    const {
+        destinationDir: OUT_DIR,
+        sourceDir: SRC_DIR,
+    } = parseCliOptions();
 
     // check SRC_DIR is src/
     {
diff --git a/tools/package_json_rewriter/main.mjs b/tools/package_json_rewriter/main.mjs
index ca85493e..ff0dc4d9 100644
--- a/tools/package_json_rewriter/main.mjs
+++ b/tools/package_json_rewriter/main.mjs
@@ -2,6 +2,7 @@ import * as assert from 'node:assert/strict';
 import * as fs from 'node:fs/promises';
 import * as path from 'node:path';
 import { fileURLToPath } from 'node:url';
+import { parseArgs } from '@pkgjs/parseargs';
 
 import { addExportsFields } from './transformer/add_exports_field/main.mjs';
 import { loadJSON } from './json.mjs';
@@ -28,14 +29,42 @@ async function writePackageJSON(baseDir, outputPath, content) {
     });
 }
 
+function parseCliOptions() {
+    const CLI_OPTIONS_INPUT_MANIFEST_PATH = 'input-manifest-path';
+
+    const options = {
+        [CLI_OPTIONS_INPUT_MANIFEST_PATH]: {
+            type: 'string',
+        },
+        destination: {
+            type: 'string',
+        },
+    };
+
+    const { values } = parseArgs({
+        options,
+        strict: true,
+    });
+
+    const sourceManifestPath = values[CLI_OPTIONS_INPUT_MANIFEST_PATH];
+    assert.ok(!!sourceManifestPath, 'no source');
+
+    const destinationDir = values.destination;
+    assert.ok(!!destinationDir, 'no destinationDir');
+
+    return {
+        sourceManifestPath,
+        destinationDir,
+    };
+}
+
 (async function main() {
     console.log('====== This script generates `exports` field for `package.json` ======');
 
-    const OUTDIR = process.env.OUTDIR;
-    assert.strictEqual(typeof OUTDIR, 'string', '$OUTDIR envvar should be string');
-
-    const INPUT_MANIFEST_PATH = process.env.INPUT_MANIFEST_PATH;
-    assert.strictEqual(typeof INPUT_MANIFEST_PATH, 'string', '$MANIFEST_PATH envvar should be string');
+    const {
+        sourceManifestPath: INPUT_MANIFEST_PATH,
+        destinationDir: OUTDIR,
+    } = parseCliOptions();
 
     const json = await loadJSON(BASE_DIR, INPUT_MANIFEST_PATH);
     assert.notStrictEqual(json, null, 'Fail to parse the file list snapshot');
diff --git a/tools/parse_argv.mjs b/tools/parse_argv.mjs
deleted file mode 100644
index 55bb3fa6..00000000
--- a/tools/parse_argv.mjs
+++ /dev/null
@@ -1,78 +0,0 @@
-import * as assert from 'node:assert/strict';
-
-function isFlagKey(str) {
-    return str.startsWith('--');
-}
-
-class SimpleArgvParser {
-    #argv;
-    #currentIndex = 0;
-
-    constructor(argv) {
-        this.#argv = argv;
-        Object.seal(this);
-    }
-
-    #get(idx) {
-        const item = this.#argv[idx];
-        const result = item === undefined ? null : item;
-        return result;
-    }
-
-    current() {
-        const idx = this.#currentIndex;
-        const item = this.#get(idx);
-        return item;
-    }
-
-    lookahead() {
-        const idx = this.#currentIndex + 1;
-        const item = this.#get(idx);
-        return item;
-    }
-
-    forwardCurrentIndexTo(num) {
-        assert.ok(typeof num === 'number');
-        this.#currentIndex = this.#currentIndex + num;
-    }
-
-    next() {
-        const current = this.current();
-        if (current === null) {
-            return {
-                done: true,
-                value: undefined,
-            };
-        }
-
-        assert.ok(isFlagKey(current), `current should always starts with \`--\` but the actual is ${current}`);
-
-        const next = this.lookahead();
-        if (next === null || isFlagKey(next)) {
-            this.forwardCurrentIndexTo(1);
-            return {
-                done: false,
-                value: [current, undefined],
-            };
-        }
-
-        // eslint-disable-next-line no-magic-numbers
-        this.forwardCurrentIndexTo(2);
-        return {
-            done: false,
-            value: [current, next],
-        };
-    }
-
-    [Symbol.iterator]() {
-        return this;
-    }
-}
-
-export function parseArgs(argv) {
-    assert.ok(Array.isArray(argv), 'argv must be an array');
-
-    const parser = new SimpleArgvParser(argv);
-    const map = new Map(parser);
-    return map;
-}
diff --git a/yarn.lock b/yarn.lock
index 62a7ca38..023f2e76 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1083,6 +1083,11 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
+"@pkgjs/parseargs@^0.9.0":
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.9.0.tgz#788e038966e668f91b160b3c4f8ca71a47408374"
+  integrity sha512-jUn59xyDdWEOZCRxH+ISzq1nVLLLLbyqX69v5VoiHx3kp22FbkXgj7wrwtkDwjZmQj3rhB3gcWQIC13JH6pd7w==
+
 "@types/json-schema@^7.0.9":
   version "7.0.9"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"


Test patch:
diff --git a/tools/test_esmodule_path_rewrite.mjs b/tools/test_esmodule_path_rewrite.mjs
index 5f473bb7..f0c2e76d 100644
--- a/tools/test_esmodule_path_rewrite.mjs
+++ b/tools/test_esmodule_path_rewrite.mjs
@@ -2,6 +2,7 @@ import * as assert from 'node:assert/strict';
 import * as fs from 'node:fs/promises';
 import * as path from 'node:path';
 import { fileURLToPath } from 'node:url';
+import { parseArgs } from '@pkgjs/parseargs';
 
 const THIS_FILENAME = fileURLToPath(import.meta.url);
 const THIS_DIRNAME = path.dirname(THIS_FILENAME);
@@ -34,11 +35,30 @@ async function testAllowToLoadFileAsESM(expectedSet) {
     assert.deepStrictEqual([], hasError, 'Unexpected files which could not load as ES Module');
 }
 
+function parseCliOptions() {
+    const options = {
+        target: {
+            type: 'string',
+        },
+    };
+
+    const { values } = parseArgs({
+        options,
+        strict: true,
+    });
+
+    const targetDir = values.target;
+    assert.ok(!!targetDir, 'no targetDir');
+
+    return {
+        targetDir,
+    };
+}
+
 (async function main() {
     console.log(`====== This script tests whether the mjs' import path is correctly ======`);
 
-    const OUTDIR = process.env.OUTDIR;
-    assert.strictEqual(typeof OUTDIR, 'string', '$OUTDIR envvar should be string');
+    const { targetDir: OUTDIR } = parseCliOptions();
 
     const json = await fs.readFile(path.resolve(THIS_DIRNAME, './pkg_files.json'), {
         encoding: 'utf8',
diff --git a/tools/test_package_contains_expected_all.mjs b/tools/test_package_contains_expected_all.mjs
index 6cbfe253..4b4dccd1 100644
--- a/tools/test_package_contains_expected_all.mjs
+++ b/tools/test_package_contains_expected_all.mjs
@@ -2,6 +2,7 @@ import * as assert from 'node:assert/strict';
 import * as fs from 'node:fs/promises';
 import * as path from 'node:path';
 import { fileURLToPath } from 'node:url';
+import { parseArgs } from '@pkgjs/parseargs';
 
 const THIS_FILENAME = fileURLToPath(import.meta.url);
 const THIS_DIRNAME = path.dirname(THIS_FILENAME);
@@ -53,11 +54,32 @@ async function testExpectedFilesInDistDir(expectedSet, fileIter) {
     assert.deepStrictEqual([], notFoundInExpected, 'unexpected files should not be included by the package');
 }
 
+function parseCliOptions() {
+    const options = {
+        target: {
+            type: 'string',
+        },
+    };
+
+    const { values } = parseArgs({
+        options,
+        strict: true,
+    });
+
+    const targetDir = values.target;
+    assert.ok(!!targetDir, 'no targetDir');
+
+    return {
+        targetDir,
+    };
+}
+
 (async function main() {
     console.log('====== This script tests whether there is a file defined by the list ======');
 
-    const OUTDIR = process.env.OUTDIR;
-    assert.strictEqual(typeof OUTDIR, 'string', '$OUTDIR envvar should be string');
+    const {
+        targetDir: OUTDIR,
+    } = parseCliOptions();
 
     const json = await fs.readFile(path.resolve(THIS_DIRNAME, './pkg_files.json'), {
         encoding: 'utf8',