Commit f35364b

Anton Golub <antongolub@antongolub.com>
2025-06-28 10:03:22
refactor: accept numeric strings as `parseDuration` arg (#1249)
* docs: add example for `ProcessPromise.nothrow(false)` * refactor: accept numeric strings as `parseDuration` arg
1 parent b0ea06e
build/cli.js
build/index.cjs
@@ -52,13 +52,13 @@ var import_vendor2 = require("./vendor.cjs");
 __reExport(index_exports, require("./core.cjs"), module.exports);
 
 // src/goods.ts
+var import_node_buffer = require("buffer");
+var import_node_process = __toESM(require("process"), 1);
 var import_node_readline = require("readline");
 var import_node_stream = require("stream");
 var import_core = require("./core.cjs");
 var import_util = require("./util.cjs");
 var import_vendor = require("./vendor.cjs");
-var import_node_buffer = require("buffer");
-var import_node_process = __toESM(require("process"), 1);
 var parseArgv = (args = import_node_process.default.argv.slice(2), opts = {}, defs = {}) => Object.entries((0, import_vendor.minimist)(args, opts)).reduce(
   (m, [k, v]) => {
     const kTrans = opts.camelCase ? import_util.toCamelCase : import_util.identity;
build/util.cjs
@@ -90,10 +90,9 @@ function parseDuration(d) {
     if (isNaN(d) || d < 0) throw new Error(`Invalid duration: "${d}".`);
     return d;
   }
-  if (/^\d+s$/.test(d)) return +d.slice(0, -1) * 1e3;
-  if (/^\d+ms$/.test(d)) return +d.slice(0, -2);
-  if (/^\d+m$/.test(d)) return +d.slice(0, -1) * 1e3 * 60;
-  throw new Error(`Unknown duration: "${d}".`);
+  const [m, v, u] = d.match(/^(\d+)(m?s?)$/) || [];
+  if (!m) throw new Error(`Unknown duration: "${d}".`);
+  return +v * ({ s: 1e3, ms: 1, m: 6e4 }[u] || 1);
 }
 var once = (fn) => {
   let called = false;
build/util.d.ts
@@ -20,7 +20,7 @@ export declare function preferLocalBin(env: NodeJS.ProcessEnv, ...dirs: (string
 };
 export declare function quote(arg: string): string;
 export declare function quotePowerShell(arg: string): string;
-export type Duration = number | `${number}m` | `${number}s` | `${number}ms`;
+export type Duration = number | `${number}` | `${number}m` | `${number}s` | `${number}ms`;
 export declare function parseDuration(d: Duration): number;
 export declare const once: <T extends (...args: any[]) => any>(fn: T) => (...args: Parameters<T>) => ReturnType<T>;
 export declare const proxyOverride: <T extends object>(origin: T, ...fallbacks: any) => T;
docs/process-promise.md
@@ -301,23 +301,26 @@ Changes behavior of `$` to not throw an exception on non-zero exit codes. Equiva
 await $`grep something from-file`.nothrow()
 
 // Inside a pipe():
-
 await $`find ./examples -type f -print0`
   .pipe($`xargs -0 grep something`.nothrow())
   .pipe($`wc -l`)
+
+// Accepts a flag to switch nothrow mode for the specific command
+$.nothrow = true
+await $`echo foo`.nothrow(false)
 ```
 
 If only the `exitCode` is needed, you can use [`exitCode`](#exitcode) directly:
 
 ```js
 if (await $`[[ -d path ]]`.exitCode == 0) {
-...
+//...
 }
 
 // Equivalent of:
 
 if ((await $`[[ -d path ]]`.nothrow()).exitCode == 0) {
-...
+//...
 }
 ```
 
src/goods.ts
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import { Buffer } from 'node:buffer'
+import process from 'node:process'
 import { createInterface } from 'node:readline'
 import { Readable } from 'node:stream'
 import { $, within, ProcessOutput, type ProcessPromise } from './core.ts'
@@ -30,8 +32,6 @@ import {
   nodeFetch,
   minimist,
 } from './vendor.ts'
-import { Buffer } from 'node:buffer'
-import process from 'node:process'
 
 type ArgvOpts = minimist.Opts & { camelCase?: boolean; parseBoolean?: boolean }
 
src/util.ts
@@ -125,18 +125,22 @@ export function quotePowerShell(arg: string): string {
   return `'` + arg.replace(/'/g, "''") + `'`
 }
 
-export type Duration = number | `${number}m` | `${number}s` | `${number}ms`
+export type Duration =
+  | number
+  | `${number}`
+  | `${number}m`
+  | `${number}s`
+  | `${number}ms`
 
 export function parseDuration(d: Duration) {
   if (typeof d === 'number') {
     if (isNaN(d) || d < 0) throw new Error(`Invalid duration: "${d}".`)
     return d
   }
-  if (/^\d+s$/.test(d)) return +d.slice(0, -1) * 1000
-  if (/^\d+ms$/.test(d)) return +d.slice(0, -2)
-  if (/^\d+m$/.test(d)) return +d.slice(0, -1) * 1000 * 60
+  const [m, v, u] = d.match(/^(\d+)(m?s?)$/) || []
+  if (!m) throw new Error(`Unknown duration: "${d}".`)
 
-  throw new Error(`Unknown duration: "${d}".`)
+  return +v * ({ s: 1000, ms: 1, m: 60_000 }[u] || 1)
 }
 
 export const once = <T extends (...args: any[]) => any>(fn: T) => {
test/util.test.js
@@ -74,12 +74,12 @@ describe('util', () => {
 
   test('duration parsing works', () => {
     assert.equal(parseDuration(1000), 1000)
+    assert.equal(parseDuration('100'), 100)
     assert.equal(parseDuration('2s'), 2000)
     assert.equal(parseDuration('500ms'), 500)
     assert.equal(parseDuration('2m'), 120000)
     assert.throws(() => parseDuration('f2ms'))
     assert.throws(() => parseDuration('2mss'))
-    assert.throws(() => parseDuration('100'))
     assert.throws(() => parseDuration(NaN))
     assert.throws(() => parseDuration(-1))
   })
.size-limit.json
@@ -15,7 +15,7 @@
       "README.md",
       "LICENSE"
     ],
-    "limit": "121.10 kB",
+    "limit": "121.05 kB",
     "brotli": false,
     "gzip": false
   },
@@ -29,7 +29,7 @@
       "build/globals.js",
       "build/deno.js"
     ],
-    "limit": "812.10 kB",
+    "limit": "812.05 kB",
     "brotli": false,
     "gzip": false
   },
@@ -62,7 +62,7 @@
       "README.md",
       "LICENSE"
     ],
-    "limit": "867.60 kB",
+    "limit": "867.55 kB",
     "brotli": false,
     "gzip": false
   }