@vbarbarosh/node-helpers 3.75.0 → 3.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/README.md +29 -3
  2. package/dist/browser.js +1 -2504
  3. package/package.json +31 -11
  4. package/src/HeartbeatServer.js +24 -3
  5. package/src/HeartbeatServer.md +33 -0
  6. package/src/array_chunk.js +1 -1
  7. package/src/array_chunk.md +8 -0
  8. package/src/array_chunk_balanced.md +14 -0
  9. package/src/array_fps.md +12 -0
  10. package/src/array_gcd.js +4 -2
  11. package/src/array_gcd.md +2 -1
  12. package/src/array_group.js +3 -3
  13. package/src/array_group.md +14 -0
  14. package/src/array_group_map.js +3 -3
  15. package/src/array_group_map.md +13 -0
  16. package/src/array_index.js +2 -2
  17. package/src/array_index.md +9 -0
  18. package/src/array_lcm.js +4 -2
  19. package/src/array_lcm.md +2 -1
  20. package/src/array_max.js +11 -4
  21. package/src/array_max.md +1 -1
  22. package/src/array_min.js +11 -4
  23. package/src/array_min.md +1 -1
  24. package/src/array_permutations.js +4 -0
  25. package/src/array_permutations.md +13 -0
  26. package/src/array_sort.js +7 -2
  27. package/src/array_sort.md +13 -0
  28. package/src/array_sort_other.js +8 -5
  29. package/src/array_sort_other.md +11 -0
  30. package/src/array_sum.js +2 -2
  31. package/src/array_sum.md +8 -0
  32. package/src/array_unique.js +2 -2
  33. package/src/array_unique_last.js +2 -2
  34. package/src/assert_valid_expires_at.md +10 -0
  35. package/src/clamp.md +9 -0
  36. package/src/cli-apps/watchdog.js +5 -0
  37. package/src/cli.js +3 -1
  38. package/src/cli.md +15 -0
  39. package/src/const_stream.md +7 -0
  40. package/src/const_type.md +5 -0
  41. package/src/countdown.js +22 -5
  42. package/src/countdown.md +21 -0
  43. package/src/crypto_decrypt_aes256.md +13 -0
  44. package/src/crypto_encrypt_aes256.md +13 -0
  45. package/src/crypto_hash_md5.md +10 -0
  46. package/src/crypto_hash_sha256.md +10 -0
  47. package/src/crypto_hmac_sha256.md +11 -0
  48. package/src/date_add_hours.md +8 -0
  49. package/src/date_add_milliseconds.md +4 -0
  50. package/src/date_add_minutes.md +4 -0
  51. package/src/date_add_months.md +13 -0
  52. package/src/date_add_seconds.md +4 -0
  53. package/src/date_diff_seconds.md +4 -0
  54. package/src/date_is_leap_year.md +9 -0
  55. package/src/escape_content_disposition.js +6 -3
  56. package/src/escape_content_disposition.md +13 -0
  57. package/src/escape_regexp.md +7 -0
  58. package/src/eta.js +7 -1
  59. package/src/eta.md +18 -0
  60. package/src/factorize_ms.js +3 -0
  61. package/src/factorize_ms.md +10 -0
  62. package/src/fastdl.js +14 -3
  63. package/src/fcmp_dates.md +12 -0
  64. package/src/fcmp_default.js +7 -0
  65. package/src/fcmp_default.md +29 -0
  66. package/src/fcmp_default_desc.js +4 -7
  67. package/src/fcmp_default_desc.md +10 -0
  68. package/src/fcmp_from_spec.md +13 -0
  69. package/src/fcmp_numbers.md +11 -0
  70. package/src/fcmp_tuples.md +17 -0
  71. package/src/fcmp_utf8_bin.md +14 -0
  72. package/src/fcmp_utf8_ci.md +14 -0
  73. package/src/fcmp_utf8_cs.md +15 -0
  74. package/src/fcmp_utf8_natural_bin.js +38 -0
  75. package/src/fcmp_utf8_natural_bin.md +8 -0
  76. package/src/fcmp_utf8_natural_ci.md +13 -0
  77. package/src/fcmp_utf8_natural_cs.md +13 -0
  78. package/src/fcmpx.md +26 -9
  79. package/src/filter1_from_spec.js +3 -3
  80. package/src/format_bytes.md +15 -0
  81. package/src/format_date.md +7 -0
  82. package/src/format_date_fs.md +7 -0
  83. package/src/format_date_human.md +6 -0
  84. package/src/format_date_ymd.md +6 -0
  85. package/src/format_error_report.js +3 -3
  86. package/src/format_error_report.md +12 -0
  87. package/src/format_kilo.md +13 -0
  88. package/src/format_ms.js +3 -2
  89. package/src/format_ms.md +12 -0
  90. package/src/format_ms2.js +6 -3
  91. package/src/format_ms2.md +12 -0
  92. package/src/format_ms3.js +3 -2
  93. package/src/format_ms3.md +12 -0
  94. package/src/format_percents.md +10 -0
  95. package/src/format_progress_bytes.md +13 -0
  96. package/src/format_progress_kilo.md +10 -0
  97. package/src/format_seconds.md +11 -0
  98. package/src/format_thousands.md +9 -0
  99. package/src/fs_append.md +7 -0
  100. package/src/fs_append_json.md +8 -0
  101. package/src/fs_append_utf8.md +6 -0
  102. package/src/fs_assert_file_exists.md +3 -0
  103. package/src/fs_assert_file_readable.js +1 -1
  104. package/src/fs_assert_file_readable.md +3 -0
  105. package/src/fs_assert_file_socket.md +2 -0
  106. package/src/fs_copy.md +7 -0
  107. package/src/fs_copy_excl.md +2 -0
  108. package/src/fs_exists.md +6 -0
  109. package/src/fs_fclose.md +5 -0
  110. package/src/fs_fi.js +13 -1
  111. package/src/fs_fi.md +11 -0
  112. package/src/fs_file_md5.md +8 -0
  113. package/src/fs_find.md +7 -0
  114. package/src/fs_fopen.md +6 -0
  115. package/src/fs_fread.js +2 -2
  116. package/src/fs_fread.md +11 -0
  117. package/src/fs_fsize.md +2 -0
  118. package/src/fs_fstat.md +2 -0
  119. package/src/fs_fwrite.js +2 -2
  120. package/src/fs_fwrite.md +9 -0
  121. package/src/fs_list.md +5 -0
  122. package/src/fs_list_deep.md +5 -0
  123. package/src/fs_ls.md +2 -0
  124. package/src/fs_lsr.md +4 -0
  125. package/src/fs_lstat.md +3 -0
  126. package/src/fs_mkdir.md +3 -0
  127. package/src/fs_mkdirp.js +3 -30
  128. package/src/fs_mkdirp.md +7 -0
  129. package/src/fs_path_basename.md +2 -0
  130. package/src/fs_path_dirname.md +2 -0
  131. package/src/fs_path_extname.md +9 -0
  132. package/src/fs_path_join.md +2 -0
  133. package/src/fs_path_relative.md +2 -0
  134. package/src/fs_path_resolve.md +2 -0
  135. package/src/fs_path_tempdir.md +2 -0
  136. package/src/fs_path_untildify.md +8 -0
  137. package/src/fs_read.md +3 -0
  138. package/src/fs_read_buffer.md +5 -0
  139. package/src/fs_read_json.md +6 -0
  140. package/src/fs_read_lines.md +7 -0
  141. package/src/fs_read_stream.md +2 -0
  142. package/src/fs_read_utf8.md +5 -0
  143. package/src/fs_readdir.md +3 -0
  144. package/src/fs_readlink.md +6 -0
  145. package/src/fs_rename.md +6 -0
  146. package/src/fs_rm.md +4 -0
  147. package/src/fs_rmdir.md +3 -0
  148. package/src/fs_rmf.md +2 -0
  149. package/src/fs_rmrf.md +8 -0
  150. package/src/fs_sha256.js +16 -0
  151. package/src/fs_sha256.md +9 -0
  152. package/src/fs_size.js +2 -2
  153. package/src/fs_size.md +6 -0
  154. package/src/fs_stat.md +2 -0
  155. package/src/fs_tempdir.js +20 -5
  156. package/src/fs_touch.md +6 -0
  157. package/src/fs_walk.js +2 -2
  158. package/src/fs_walk.md +14 -0
  159. package/src/fs_write.md +3 -0
  160. package/src/fs_write_json.md +7 -0
  161. package/src/fs_write_stream.md +2 -0
  162. package/src/gettype.md +13 -0
  163. package/src/http_delete.md +11 -0
  164. package/src/http_get_blob.md +8 -0
  165. package/src/http_get_buffer.md +10 -0
  166. package/src/http_get_file.js +8 -8
  167. package/src/http_get_file.md +14 -0
  168. package/src/http_get_json.md +10 -0
  169. package/src/http_get_stream.md +14 -0
  170. package/src/http_get_stream_range.js +27 -9
  171. package/src/http_get_stream_range.md +22 -0
  172. package/src/http_get_utf8.md +9 -0
  173. package/src/http_head.md +9 -0
  174. package/src/http_patch_json.md +10 -0
  175. package/src/http_post_json.md +11 -0
  176. package/src/http_post_multipart.md +15 -0
  177. package/src/http_post_urlencoded.md +11 -0
  178. package/src/http_put_buffer.md +9 -0
  179. package/src/http_put_file.js +9 -6
  180. package/src/http_put_file.md +16 -0
  181. package/src/http_put_json.md +11 -0
  182. package/src/http_put_utf8.md +9 -0
  183. package/src/http_range_parse.js +6 -3
  184. package/src/http_range_parse.md +18 -0
  185. package/src/http_stream_range.js +35 -22
  186. package/src/http_stream_range.md +29 -0
  187. package/src/identity.md +2 -0
  188. package/src/ignore.md +2 -0
  189. package/src/json_stringify_safe.md +10 -0
  190. package/src/json_stringify_stable.md +9 -0
  191. package/src/logger.js +2 -3
  192. package/src/logger.md +15 -0
  193. package/src/make_progress.js +8 -5
  194. package/src/make_progress.md +20 -0
  195. package/src/math_gcd.js +2 -1
  196. package/src/mongo_stream_upsert.js +10 -3
  197. package/src/mongo_stream_upsert.md +17 -0
  198. package/src/mongo_stream_write.js +10 -3
  199. package/src/mongo_stream_write.md +16 -0
  200. package/src/msval.md +10 -0
  201. package/src/now_fs.md +7 -0
  202. package/src/now_human.md +6 -0
  203. package/src/object_defaults.md +11 -0
  204. package/src/object_schema.js +2 -1
  205. package/src/object_schema.md +12 -0
  206. package/src/object_walk_preorder.md +16 -0
  207. package/src/parallel.js +23 -10
  208. package/src/parse_bytes.js +4 -2
  209. package/src/parse_bytes.md +15 -0
  210. package/src/parse_hms.js +1 -1
  211. package/src/parse_http_content_range.js +9 -5
  212. package/src/parse_http_content_range.md +16 -0
  213. package/src/perf_end_human.md +10 -0
  214. package/src/perf_end_ms.md +8 -0
  215. package/src/perf_measure.md +10 -0
  216. package/src/perf_measure_human.md +8 -0
  217. package/src/perf_start.md +9 -0
  218. package/src/pgid_exists.md +6 -0
  219. package/src/pgid_kill_grace.js +1 -1
  220. package/src/pgid_kill_grace.md +15 -0
  221. package/src/pid_exists.md +5 -0
  222. package/src/pid_kill_grace.js +1 -1
  223. package/src/pid_kill_grace.md +14 -0
  224. package/src/plural.md +13 -0
  225. package/src/random_hex.md +9 -0
  226. package/src/random_int.md +8 -0
  227. package/src/range.js +4 -2
  228. package/src/range.md +2 -0
  229. package/src/redis_connect.js +1 -1
  230. package/src/redis_connect.md +10 -0
  231. package/src/redis_lshift.md +14 -0
  232. package/src/redis_poll.js +1 -0
  233. package/src/redis_poll.md +17 -0
  234. package/src/redis_poll_zshift_callback_rpush.js +1 -2
  235. package/src/redis_poll_zshift_callback_rpush.md +17 -0
  236. package/src/redis_poll_zshift_spawn_rpush.js +2 -1
  237. package/src/redis_poll_zshift_spawn_rpush.md +20 -0
  238. package/src/redis_zshift.md +14 -0
  239. package/src/round.js +7 -8
  240. package/src/round.md +15 -0
  241. package/src/sanitize_dash_name.md +13 -0
  242. package/src/sanitize_filename.md +9 -0
  243. package/src/sanitize_var_name.md +14 -0
  244. package/src/sftp_get_file_info.js +2 -2
  245. package/src/sftp_get_file_info.md +14 -0
  246. package/src/sftp_get_stream_range.js +32 -22
  247. package/src/sftp_get_stream_range.md +20 -0
  248. package/src/shell_curl_progress.js +2 -1
  249. package/src/shell_curl_progress.md +17 -0
  250. package/src/shell_lines.js +2 -2
  251. package/src/shell_lines.md +1 -1
  252. package/src/shell_spawn.js +7 -0
  253. package/src/shell_ytdlp_progress.js +2 -1
  254. package/src/shell_ytdlp_progress.md +17 -0
  255. package/src/str_camel_to_snake.md +11 -0
  256. package/src/str_parse_kv.md +11 -0
  257. package/src/stream_curl_progress.md +14 -0
  258. package/src/stream_data_ln.js +7 -1
  259. package/src/stream_discard.md +8 -0
  260. package/src/stream_group.md +3 -0
  261. package/src/stream_gunzip.md +7 -0
  262. package/src/stream_hash.md +10 -0
  263. package/src/stream_map.js +8 -1
  264. package/src/stream_map_parallel.js +9 -1
  265. package/src/stream_md5.md +9 -0
  266. package/src/stream_multiplex.js +11 -0
  267. package/src/stream_multiplex.md +16 -0
  268. package/src/stream_through.md +3 -0
  269. package/src/stream_transform.md +2 -0
  270. package/src/stream_xml_analyze.js +9 -1
  271. package/src/stream_xml_parse.js +9 -1
  272. package/src/stream_ytdlp_progress.js +1 -1
  273. package/src/stream_ytdlp_progress.md +18 -0
  274. package/src/strtr.js +15 -5
  275. package/src/strtr.md +11 -0
  276. package/src/urlclean.js +9 -2
  277. package/src/urlmod.js +5 -0
  278. package/src/urlparts.md +17 -0
  279. package/src/wait_while.js +1 -1
  280. package/src/wait_while.md +1 -1
  281. package/src/HeartbeatServer.test.js +0 -31
  282. package/src/array_chunk.test.js +0 -24
  283. package/src/array_chunk_balanced.test.js +0 -37
  284. package/src/array_fps.test.js +0 -72
  285. package/src/array_gcd.test.js +0 -10
  286. package/src/array_index.test.js +0 -14
  287. package/src/array_lcm.test.js +0 -12
  288. package/src/array_max.test.js +0 -20
  289. package/src/array_min.test.js +0 -20
  290. package/src/array_permutations.test.js +0 -20
  291. package/src/array_sort.test.js +0 -85
  292. package/src/array_sort_other.test.js +0 -9
  293. package/src/array_sum.test.js +0 -11
  294. package/src/cli-apps/watchdog.d/connect_close.js +0 -17
  295. package/src/cli-apps/watchdog.d/connect_close2.js +0 -20
  296. package/src/cli-apps/watchdog.d/connect_close3.js +0 -25
  297. package/src/cli-apps/watchdog.d/connect_no_ping.js +0 -13
  298. package/src/cli-apps/watchdog.d/early_exit.js +0 -7
  299. package/src/cli-apps/watchdog.d/flood.js +0 -13
  300. package/src/cli-apps/watchdog.d/flood2.js +0 -15
  301. package/src/cli-apps/watchdog.d/hang.js +0 -8
  302. package/src/cli-apps/watchdog.d/huge_delay.js +0 -18
  303. package/src/cli-apps/watchdog.d/ignore_sigterm.js +0 -29
  304. package/src/cli-apps/watchdog.d/late_heartbeat.js +0 -21
  305. package/src/cli-apps/watchdog.d/my-exit-after-2-pings.js +0 -16
  306. package/src/cli-apps/watchdog.d/my-happy-path.js +0 -16
  307. package/src/cli-apps/watchdog.d/my-ignore-sigterm.js +0 -22
  308. package/src/cli-apps/watchdog.d/my-never-ping.js +0 -15
  309. package/src/cli-apps/watchdog.d/no_heartbeat.js +0 -5
  310. package/src/cli-apps/watchdog.d/ok.js +0 -17
  311. package/src/cli-apps/watchdog.d/quick-exit-after-2-pings.js +0 -15
  312. package/src/cli-apps/watchdog.d/quick-hang-after-1-ping.js +0 -15
  313. package/src/cli-apps/watchdog.d/spawn_children.js +0 -14
  314. package/src/cli-apps/watchdog.test.js +0 -48
  315. package/src/crypto_encrypt_aes256.test.js +0 -10
  316. package/src/crypto_hash_sha256.test.js +0 -10
  317. package/src/date_add_months.test.js +0 -40
  318. package/src/date_is_leap_year.test.js +0 -14
  319. package/src/escape_content_disposition.test.js +0 -22
  320. package/src/fcmp_from_spec.test.js +0 -91
  321. package/src/fcmpx.test.js +0 -173
  322. package/src/filter1_from_spec.test.js +0 -31
  323. package/src/format_bytes.test.js +0 -22
  324. package/src/format_error_report.d/src/1.js +0 -26
  325. package/src/format_error_report.d/src/2.js +0 -29
  326. package/src/format_kilo.test.js +0 -25
  327. package/src/format_ms.test.js +0 -33
  328. package/src/format_ms2.test.js +0 -33
  329. package/src/format_ms3.test.js +0 -33
  330. package/src/format_percents.test.js +0 -22
  331. package/src/format_progress_bytes.test.js +0 -19
  332. package/src/format_progress_kilo.test.js +0 -19
  333. package/src/format_seconds.test.js +0 -42
  334. package/src/format_thousands.test.js +0 -28
  335. package/src/fs_file_md5.test.js +0 -11
  336. package/src/fs_mkdirp.test.js +0 -24
  337. package/src/fs_path_extname.test.js +0 -22
  338. package/src/gettype.test.js +0 -27
  339. package/src/object_schema.test.js +0 -15
  340. package/src/object_walk_preorder.test.js +0 -17
  341. package/src/parallel.test.js +0 -11
  342. package/src/parse_bytes.test.js +0 -21
  343. package/src/parse_hms.test.js +0 -32
  344. package/src/parse_http_content_range.test.js +0 -14
  345. package/src/pid_kill_grace.d/ignore-sigterm-for-10ms.js +0 -33
  346. package/src/pid_kill_grace.d/ignore-sigterm.js +0 -22
  347. package/src/pid_kill_grace.test.js +0 -63
  348. package/src/ping_socket.test.js +0 -58
  349. package/src/plural.test.js +0 -20
  350. package/src/range.test.js +0 -41
  351. package/src/redis_lshift.test.js +0 -57
  352. package/src/redis_zshift.test.js +0 -58
  353. package/src/round.test.js +0 -56
  354. package/src/sanitize_dash_name.test.js +0 -59
  355. package/src/sanitize_var_name.test.js +0 -39
  356. package/src/shell_spawn.test.js +0 -30
  357. package/src/str_camel_to_snake.test.js +0 -17
  358. package/src/stream_curl_progress.d/basic +0 -3
  359. package/src/stream_curl_progress.d/basic.json +0 -42
  360. package/src/stream_curl_progress.test.js +0 -17
  361. package/src/stream_data_ln.test.js +0 -24
  362. package/src/stream_map_parallel.test.js +0 -26
  363. package/src/stream_multiplex.test.js +0 -67
  364. package/src/stream_strpbrk.test.js +0 -29
  365. package/src/urlclean.test.js +0 -18
  366. package/src/urlmod.test.js +0 -40
  367. package/src/urlparts.test.js +0 -294
@@ -0,0 +1,13 @@
1
+ Locale-aware natural string comparison, case-sensitive: `localeCompare` with
2
+ `{numeric: true, sensitivity: 'variant'}`. Digit chunks compare as numbers
3
+ (`'file2' < 'file10'`), base letters dominate (`'apple2' < 'Banana1'`), and
4
+ case/accents distinguish otherwise equal strings.
5
+
6
+ ```js
7
+ ['a10', 'a2', 'A20', 'A3'].sort(fcmp_utf8_natural_cs) // 'a2' 'A3' 'a10' 'A20'
8
+ ```
9
+
10
+ Results depend on the runtime locale/ICU — the exact order of case variants
11
+ varies between machines. For deterministic natural order (uppercase before
12
+ lowercase, identical everywhere) use `fcmp_utf8_natural_bin`, the string order
13
+ behind `fcmp_default`/`fcmpx`. For ignoring case use `fcmp_utf8_natural_ci`.
package/src/fcmpx.md CHANGED
@@ -1,17 +1,34 @@
1
- Comparator builder using compact expressions.
1
+ Comparator builder using compact expressions: creates an `fcmp` function
2
+ suitable for `[].sort()`. Values compare with `fcmp_default` (natural order —
3
+ see `fcmp_default.md`) unless a custom `fcmp` is given.
2
4
 
3
- fcmp expression — Creates an `fcmp` function from an expression, suitable for use with `[].sort()`.
5
+ **Expression forms** (composable — an array mixes any of them as sort keys,
6
+ first key wins, later keys break ties):
4
7
 
5
8
  ```js
6
- fcmpx('user.email')
7
- fcmpx('-user.age,user.email')
8
- fcmpx(v => v.user.email)
9
- fcmpx(['user.age', 'user.email'])
10
- fcmpx(['-user.age', 'user.email'])
9
+ fcmpx('user.email') // dotted property path
10
+ fcmpx('-user.age') // '-' prefix → descending
11
+ fcmpx('-user.age,user.email') // comma → multiple keys
12
+ fcmpx(v => v.user.email) // read function
13
+ fcmpx('') // '' or '.' → the item itself
14
+ fcmpx(['-age', 'name']) // array of keys
15
+ fcmpx({read: 'v', fcmp: (a, b) => b - a}) // object form: custom comparator
16
+ fcmpx({read: 'type', top: ['dir']}) // listed values first, in listed order
17
+ fcmpx({read: 'type', bottom: ['tmp']}) // listed values last
18
+ fcmpx({read: 'age', desc: true}) // descending via object form
11
19
  ```
12
20
 
13
- Sorting rule: Missing values are considered greater than any defined value
14
- and are therefore placed after all defined values.
21
+ ```js
22
+ users.sort(fcmpx('-age,name'));
23
+ // age 35 first, ties by name; {name: 'bob'} (no age) goes last
24
+
25
+ files.sort(fcmpx([{read: 'type', top: ['dir', 'file']}, 'name']));
26
+ // dirs, then files, then everything else — each group sorted by name
27
+ ```
28
+
29
+ **Missing values:** `null`/`undefined` (and dead-end paths) are considered
30
+ greater than any defined value, so they always land after all defined values —
31
+ even in descending order.
15
32
 
16
33
  - YouTube: [Сортировка объектов в JavaScript](https://www.youtube.com/watch?v=rHEH_JfQjL4)
17
34
  - https://github.com/vbarbarosh/speaking/tree/master/20250822-js-sort-fcmpx
@@ -39,8 +39,8 @@ function parse_spec(spec)
39
39
  function parse_expr(expr)
40
40
  {
41
41
  let substr;
42
- let starts = false; // ^
43
- let ends = false; // $
42
+ let starts; // ^
43
+ let ends; // $
44
44
  substr = expr.replaceAll('^', '');
45
45
  starts = (substr.length !== expr.length);
46
46
  expr = substr;
@@ -50,7 +50,7 @@ function parse_expr(expr)
50
50
  substr = expr.replaceAll('!', '');
51
51
  if ((expr.length - substr.length) % 2) { // not
52
52
  if (starts && ends) {
53
- return s => !s.startsWith(substr) && !s.endsWith(substr);
53
+ return s => !(s.startsWith(substr) && s.endsWith(substr));
54
54
  }
55
55
  if (starts) {
56
56
  return s => !s.startsWith(substr);
@@ -0,0 +1,15 @@
1
+ Formats a byte count as a human-readable size using base-1024 math with
2
+ `B`/`KB`/`MB`/`GB`/`TB`/`PB` suffixes.
3
+
4
+ - Non-number or `NaN` input returns `'n/a'`.
5
+ - `0` returns `'0KB'`; any other value below 1024 returns `'1KB'` — sizes are
6
+ never shown in bytes or fractions of a KB.
7
+ - Values up to 1 MB are rounded to a whole number; above 1 MB two decimals are
8
+ kept, with a trailing `.00` collapsed to `.0`.
9
+
10
+ ```js
11
+ format_bytes(500) // '1KB'
12
+ format_bytes(2048) // '2KB'
13
+ format_bytes(2*1024*1024) // '2.0MB'
14
+ format_bytes(35.45*1024*1024) // '35.45MB'
15
+ ```
@@ -0,0 +1,7 @@
1
+ Formats a `Date` as `YYYY/MM/DD HH:MM:SS` in local time, all fields
2
+ zero-padded to two digits. Same output as `format_date_human`, which is the
3
+ more readable implementation.
4
+
5
+ ```js
6
+ format_date(new Date('2026/07/06 09:05:03')) // '2026/07/06 09:05:03'
7
+ ```
@@ -0,0 +1,7 @@
1
+ Formats a `Date` as `YYYYMMDD_HHMMSS` in local time — no separators except the
2
+ underscore, so the result is safe to use in file names and sorts
3
+ chronologically.
4
+
5
+ ```js
6
+ format_date_fs(new Date('2026/07/06 09:05:03')) // '20260706_090503'
7
+ ```
@@ -0,0 +1,6 @@
1
+ Formats a `Date` as `YYYY/MM/DD HH:MM:SS` in local time, all fields
2
+ zero-padded to two digits. Produces the same output as `format_date`.
3
+
4
+ ```js
5
+ format_date_human(new Date('2026/07/06 09:05:03')) // '2026/07/06 09:05:03'
6
+ ```
@@ -0,0 +1,6 @@
1
+ Formats the date part of a `Date` as `YYYY/MM/DD` in local time, month and day
2
+ zero-padded to two digits.
3
+
4
+ ```js
5
+ format_date_ymd(new Date('2026/07/06 09:05:03')) // '2026/07/06'
6
+ ```
@@ -37,11 +37,11 @@ ${JSON.stringify(error.response.config.headers||{}, null, 4).slice(1, -1).replac
37
37
 
38
38
  ${error.response.status} ${error.response.statusText}
39
39
 
40
- ${(error.response.data.toString()||'').slice(0, 10240) || 'n/a'}
40
+ ${String(error.response.data ?? '').slice(0, 10240) || 'n/a'}
41
41
 
42
42
  --- STACK ---
43
43
 
44
- ${error.stack ? error.stack.split(/\\n\\s*/) : 'n/a'}
44
+ ${error.stack ?? 'n/a'}
45
45
  `.trimStart();
46
46
  }
47
47
 
@@ -58,7 +58,7 @@ ${JSON.stringify(error.config.headers||{}, null, 4).slice(1, -1).replace(/^\s+|,
58
58
 
59
59
  --- STACK ---
60
60
 
61
- ${error.stack ? error.stack.split(/\\n\\s*/) : 'n/a'}
61
+ ${error.stack ?? 'n/a'}
62
62
  `.trimStart();
63
63
  }
64
64
 
@@ -0,0 +1,12 @@
1
+ Formats an error into a multi-line text report suitable for logs. Axios
2
+ errors get special treatment; everything else becomes indented JSON.
3
+
4
+ **Rules:**
5
+ - Axios error with a response (`error.response.status/statusText/config`) —
6
+ plain-text report with `--- REQUEST ---` (method, url, headers),
7
+ `--- RESPONSE ---` (status, first 10240 chars of body), and `--- STACK ---` sections.
8
+ - Axios error without a response (`error.config`) — same, minus the RESPONSE section.
9
+ - Any other truthy error — JSON with `code`, `name`, `message`, and `stack`
10
+ split into an array of lines (circular-safe via `json_stringify_safe`).
11
+ - Falsy error — `{"error":null}`-style JSON; `undefined` is reported as the
12
+ string `'---undefined---'`.
@@ -0,0 +1,13 @@
1
+ Formats a number in metric (base-1000) notation with `K`/`M`/`G`/`T`/`P`
2
+ suffixes.
3
+
4
+ - Non-number or `NaN` input returns `'n/a'`.
5
+ - `0` returns `'0'`; values below 1000 are rounded to a whole number, no
6
+ suffix.
7
+ - Values of 1000 and above always keep two decimals.
8
+
9
+ ```js
10
+ format_kilo(55.55) // '56'
11
+ format_kilo(1500) // '1.50K'
12
+ format_kilo(35.45e6) // '35.45M'
13
+ ```
package/src/format_ms.js CHANGED
@@ -10,11 +10,12 @@ function format_ms(ms)
10
10
  }
11
11
 
12
12
  // 00:05
13
+ const sign = ms < 0 ? '-' : '';
13
14
  const [h, m, s] = factorize_ms(ms);
14
15
  if (h) {
15
- return `0${h}:0${m}:0${s}`.replace(/0+(?=\d\d)/g, '');
16
+ return sign + `0${h}:0${m}:0${s}`.replace(/0+(?=\d\d)/g, '');
16
17
  }
17
- return `0${m}:0${s}`.replace(/0+(?=\d\d)/g, '');
18
+ return sign + `0${m}:0${s}`.replace(/0+(?=\d\d)/g, '');
18
19
  }
19
20
 
20
21
  module.exports = format_ms;
@@ -0,0 +1,12 @@
1
+ Formats a millisecond duration as `MM:SS`, or `HH:MM:SS` when there is a
2
+ nonzero hours part (via `factorize_ms`). Fields are zero-padded to two digits;
3
+ the hours field grows as needed (`'999:00:00'`). Sub-second precision is
4
+ truncated, not rounded. Falsy input formats as `0`; negative input is
5
+ factorized by magnitude and prefixed with `'-'`.
6
+
7
+ ```js
8
+ format_ms(5445) // '00:05'
9
+ format_ms(1500.7) // '00:01'
10
+ format_ms(3661000) // '01:01:01'
11
+ format_ms(-61000) // '-01:01'
12
+ ```
package/src/format_ms2.js CHANGED
@@ -10,11 +10,14 @@ function format_ms2(ms)
10
10
  }
11
11
 
12
12
  // 00:05.45
13
- const [h, m, s, xx] = factorize_ms(ms);
13
+ // Round to centiseconds before factorizing so the carry propagates:
14
+ // rounding the factorized remainder would turn 5999 into "00:05.100"
15
+ const sign = ms < 0 ? '-' : '';
16
+ const [h, m, s, xx] = factorize_ms(Math.round(Math.abs(ms)/10)*10);
14
17
  if (h) {
15
- return `0${h}:0${m}:0${s}.0${Math.round(xx/10)}`.replace(/0+(?=\d\d)/g, '');
18
+ return sign + `0${h}:0${m}:0${s}.0${xx/10}`.replace(/0+(?=\d\d)/g, '');
16
19
  }
17
- return `0${m}:0${s}.0${Math.round(xx/10)}`.replace(/0+(?=\d\d)/g, '');
20
+ return sign + `0${m}:0${s}.0${xx/10}`.replace(/0+(?=\d\d)/g, '');
18
21
  }
19
22
 
20
23
  module.exports = format_ms2;
@@ -0,0 +1,12 @@
1
+ Formats a millisecond duration as `MM:SS.cc` (centiseconds), or `HH:MM:SS.cc`
2
+ when there is a nonzero hours part. The value is rounded to the nearest
3
+ centisecond *before* factorizing, so the carry propagates through the larger
4
+ units. Falsy input formats as `0`; negative input is factorized by magnitude
5
+ and prefixed with `'-'`.
6
+
7
+ ```js
8
+ format_ms2(5445) // '00:05.45'
9
+ format_ms2(5999) // '00:06.00' (rounding carries into seconds)
10
+ format_ms2(3599999) // '01:00:00.00' (…and all the way up)
11
+ format_ms2(-3661500) // '-01:01:01.50'
12
+ ```
package/src/format_ms3.js CHANGED
@@ -10,11 +10,12 @@ function format_ms3(ms, include_zero_hours = false)
10
10
  }
11
11
 
12
12
  // 00:05.445
13
+ const sign = ms < 0 ? '-' : '';
13
14
  const [h, m, s, xx] = factorize_ms(ms);
14
15
  if (h || include_zero_hours) {
15
- return `0${h}:0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '');
16
+ return sign + `0${h}:0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '');
16
17
  }
17
- return `0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '')
18
+ return sign + `0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '')
18
19
  }
19
20
 
20
21
  module.exports = format_ms3;
@@ -0,0 +1,12 @@
1
+ Formats a millisecond duration as `MM:SS.mmm` (full milliseconds), or
2
+ `HH:MM:SS.mmm` when there is a nonzero hours part. Sub-millisecond fractions
3
+ are truncated, not rounded. Pass `include_zero_hours = true` to force the
4
+ hours field even when it is zero. Falsy input formats as `0`; negative input
5
+ is factorized by magnitude and prefixed with `'-'`.
6
+
7
+ ```js
8
+ format_ms3(5445) // '00:05.445'
9
+ format_ms3(5445, true) // '00:00:05.445'
10
+ format_ms3(1500.7) // '00:01.500'
11
+ format_ms3(-3661500) // '-01:01:01.500'
12
+ ```
@@ -0,0 +1,10 @@
1
+ Formats a fraction (`0` to `1`) as a percentage with two decimal places. The
2
+ exact endpoints drop the decimals: `0` gives `'0%'` and `1` gives `'100%'`,
3
+ while everything in between keeps them (`'50.00%'`).
4
+
5
+ ```js
6
+ format_percents(0) // '0%'
7
+ format_percents(0.0101) // '1.01%'
8
+ format_percents(0.5) // '50.00%'
9
+ format_percents(1) // '100%'
10
+ ```
@@ -0,0 +1,13 @@
1
+ Formats a `make_progress` snapshot (`{percents, total, done, rate, eta, duration}`)
2
+ as a one-line byte-progress string, using 1024-based units (`format_bytes`).
3
+ Unknown parts (zero/null rate, eta, total) render as `~`; the layout degrades
4
+ as less is known: full form → `done of ~` (no total) → `~ duration=…`
5
+ (nothing done) → `~`. When `done > total`, the percents/total part is
6
+ dropped entirely.
7
+
8
+ ```js
9
+ format_progress_bytes({percents: 0.2525, total: 15*1024*1024, done: 5*1024*1024, rate: 10*1024*1024, eta: 5, duration: 1})
10
+ // '25.25% | 5.0MB of 15.0MB at 10.0MB/s ETA 00:00:05 duration=00:00:01'
11
+ format_progress_bytes({percents: 0, total: 0, done: 0, rate: 0, eta: 0, duration: 0})
12
+ // '~'
13
+ ```
@@ -0,0 +1,10 @@
1
+ Formats a `make_progress` snapshot (`{percents, total, done, rate, eta, duration}`)
2
+ as a one-line progress string using metric 1000-based units (`format_kilo`) —
3
+ for counting items, jobs, rows, etc. Identical layout and degradation rules
4
+ to `format_progress_bytes`: unknown parts render as `~`, and when
5
+ `done > total` the percents/total part is dropped.
6
+
7
+ ```js
8
+ format_progress_kilo({percents: 0.2525, total: 15e6, done: 5e6, rate: 10e6, eta: 5, duration: 1})
9
+ // '25.25% | 5.00M of 15.00M at 10.00M/s ETA 00:00:05 duration=00:00:01'
10
+ ```
@@ -0,0 +1,11 @@
1
+ Formats a duration in seconds as `HH:MM:SS`. Hours are always present and grow
2
+ beyond two digits as needed. Fractional seconds are truncated toward zero. The
3
+ `'-'` prefix appears only for `seconds <= -1`, so values in `(-1, 0)` format as
4
+ plain `'00:00:00'`.
5
+
6
+ ```js
7
+ format_seconds(3661) // '01:01:01'
8
+ format_seconds(1000000) // '277:46:40'
9
+ format_seconds(-0.9) // '00:00:00'
10
+ format_seconds(-1) // '-00:00:01'
11
+ ```
@@ -0,0 +1,9 @@
1
+ Formats a number with commas as thousands separators. Works on the value's
2
+ `toString()` output: only the integer part is grouped, fractional digits are
3
+ left untouched, and negative numbers are handled.
4
+
5
+ ```js
6
+ format_thousands(1234567) // '1,234,567'
7
+ format_thousands(-1000) // '-1,000'
8
+ format_thousands(123456789.01234) // '123,456,789.01234'
9
+ ```
@@ -0,0 +1,7 @@
1
+ Appends data to a file, creating the file if it does not exist.
2
+ Thin wrapper over `fs.promises.appendFile(file, data, options)`; `data` may be a string or a `Buffer`.
3
+ See `fs_append_utf8` for text and `fs_append_json` for JSON lines.
4
+
5
+ ```js
6
+ fs_append(file, data, options) → Promise<void>
7
+ ```
@@ -0,0 +1,8 @@
1
+ Appends a value to a file as one line of JSON (JSON Lines format).
2
+ The value is serialized with `json_stringify_stable` (object keys sorted) and followed by a newline.
3
+
4
+ ```js
5
+ await fs_append_json(file, {b: 1, a: [2]});
6
+ await fs_append_json(file, 'x');
7
+ // file content: '{"a":[2],"b":1}\n"x"\n'
8
+ ```
@@ -0,0 +1,6 @@
1
+ Appends text to a file, creating the file if it does not exist.
2
+ Same wrapper over `fs.promises.appendFile` as `fs_append`; the name documents the intent (UTF-8 is the default encoding).
3
+
4
+ ```js
5
+ fs_append_utf8(file, text, options) → Promise<void>
6
+ ```
@@ -0,0 +1,3 @@
1
+ Asserts that `file` is a regular file; resolves with nothing on success.
2
+ Rejects with `ENOENT` if the path is missing, and with `Not a file: <file>` for anything else.
3
+ The check is lstat-based (`fs_fi`), so even a symlink pointing at a regular file rejects.
@@ -6,7 +6,7 @@ async function fs_assert_file_readable(file)
6
6
  if (!fi.isFile()) {
7
7
  throw new Error(`Not a file: ${file}`);
8
8
  }
9
- if (fi.mode & 0o111) {
9
+ if ((fi.mode & 0o444) !== 0o444) {
10
10
  throw new Error(`File is not readable by all: ${file}`);
11
11
  }
12
12
  }
@@ -0,0 +1,3 @@
1
+ Asserts that `file` is a regular file readable by everyone: rejects unless the mode
2
+ includes all three read bits (`0o444`).
3
+ Like `fs_assert_file_exists`, the check is lstat-based, so symlinks fail it.
@@ -0,0 +1,2 @@
1
+ Asserts that `file` is a Unix domain socket.
2
+ Rejects with `Not a socket: <file>` for any other entry type, and with `ENOENT` if the path is missing.
package/src/fs_copy.md ADDED
@@ -0,0 +1,7 @@
1
+ Copies `src` to `dest`, overwriting `dest` if it exists.
2
+ Thin wrapper over `fs.promises.copyFile(src, dest, mode)`; `mode` takes `fs.constants` copy flags.
3
+ Use `fs_copy_excl` to fail instead of overwriting.
4
+
5
+ ```js
6
+ fs_copy(src, dest, mode) → Promise<void>
7
+ ```
@@ -0,0 +1,2 @@
1
+ Copies `src` to `dest`, rejecting with `EEXIST` if `dest` already exists (`COPYFILE_EXCL`).
2
+ Use `fs_copy` when overwriting is fine.
@@ -0,0 +1,6 @@
1
+ Resolves with `true` if `path` is accessible and `false` otherwise; never rejects.
2
+ Follows symlinks (via `fs.access`), so a broken symlink reports `false`.
3
+
4
+ ```js
5
+ fs_exists(path) → Promise<boolean>
6
+ ```
@@ -0,0 +1,5 @@
1
+ Closes a file descriptor obtained from `fs_fopen`.
2
+
3
+ ```js
4
+ fs_fclose(fp) → Promise<void>
5
+ ```
package/src/fs_fi.js CHANGED
@@ -15,7 +15,19 @@ async function fs_fi(pathname)
15
15
  const target = await fs_readlink(fi.pathname);
16
16
  const target_pathname = fs_path_resolve(fs_path_dirname(pathname), target);
17
17
  fi.target = target;
18
- fi.target_fi = await fs_lstat(target_pathname);
18
+ try {
19
+ fi.target_fi = await fs_lstat(target_pathname);
20
+ }
21
+ catch (error) {
22
+ if (!['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code)) {
23
+ throw error;
24
+ }
25
+ // A broken symlink is a legitimate directory entry: report it
26
+ // with target_fi = null instead of failing the whole listing
27
+ // (fs_find, fs_list, fs_ls, fs_lsr, fs_list_deep).
28
+ fi.target_fi = null;
29
+ return fi;
30
+ }
19
31
  fi.target_fi.pathname = target_pathname;
20
32
  fi.target_fi.basename = fs_path_basename(target_pathname);
21
33
  flags(fi.target_fi);
package/src/fs_fi.md ADDED
@@ -0,0 +1,11 @@
1
+ Returns extended file info: an lstat `Stats` object augmented with `pathname`, `basename`,
2
+ `flags` (e.g. `['is_file']`) and `flags_map` (e.g. `{is_file: true}`).
3
+ For a symlink it also resolves the target: `target` is the raw link text and `target_fi`
4
+ the target's own info object — or `null` for a broken symlink, so directory listings
5
+ (`fs_find`, `fs_list`, ...) never fail on dangling links.
6
+
7
+ ```js
8
+ await fs_fi('/tmp/d/link'); // link → 'data.txt'
9
+ // { ..., flags: ['is_symbolic_link'], target: 'data.txt',
10
+ // target_fi: { pathname: '/tmp/d/data.txt', flags: ['is_file'], ... } }
11
+ ```
@@ -0,0 +1,8 @@
1
+ Computes the MD5 of a file by streaming it (constant memory regardless of
2
+ file size). The second argument is the digest encoding, `'hex'` by default;
3
+ any `crypto` digest encoding (e.g. `'base64'`) works.
4
+
5
+ ```js
6
+ await fs_file_md5('/tmp/data.txt') // '5eb63bbbe01eeed093cb22bb8f5acdc3'
7
+ await fs_file_md5('/tmp/data.txt', 'base64') // 'XrY7u+Ae7tCTyyK7j1rNww=='
8
+ ```
package/src/fs_find.md ADDED
@@ -0,0 +1,7 @@
1
+ Recursively lists a directory tree as an array of `fs_fi` info objects with absolute
2
+ pathnames — the root entry first, then its contents depth-first in `readdir` order.
3
+ A non-directory pathname yields just its own entry.
4
+ Broken symlinks are reported (`target_fi: null`) instead of failing the walk,
5
+ and symlinks to directories are not descended into.
6
+ See `fs_list` for a shallow listing and `fs_list_deep` for a recursive one
7
+ that keeps pathnames relative to the input.
@@ -0,0 +1,6 @@
1
+ Opens a file and resolves with a numeric file descriptor for use with
2
+ `fs_fread`, `fs_fwrite`, `fs_fstat`, `fs_fsize` and `fs_fclose`.
3
+
4
+ ```js
5
+ fs_fopen(file, flags = 'r', mode = 0o666) → Promise<number>
6
+ ```
package/src/fs_fread.js CHANGED
@@ -5,12 +5,12 @@ const fs = require('fs');
5
5
  */
6
6
  async function fs_fread(fp, buffer, offset = null, size = buffer.length)
7
7
  {
8
- const {bytesRead} = await new Promise(function (resolve, reject) {
8
+ const bytes_read = await new Promise(function (resolve, reject) {
9
9
  fs.read(fp, buffer, 0, size, offset, function (error, out) {
10
10
  error ? reject(error) : resolve(out);
11
11
  });
12
12
  });
13
- return (bytesRead === buffer.length) ? buffer : buffer.subarray(0, bytesRead);
13
+ return (bytes_read === buffer.length) ? buffer : buffer.subarray(0, bytes_read);
14
14
  }
15
15
 
16
16
  module.exports = fs_fread;
@@ -0,0 +1,11 @@
1
+ Reads up to `size` bytes (default: the buffer length) from a file descriptor into the start of `buffer`.
2
+ With `offset = null` (default) it reads from the current file position and advances it;
3
+ an explicit `offset` reads at that position without moving it.
4
+ Resolves with the filled part of the buffer: `buffer` itself when filled exactly,
5
+ otherwise `buffer.subarray(0, bytes_read)` — an empty chunk past the end of file.
6
+
7
+ ```js
8
+ const fp = await fs_fopen(file); // file content: 'hello world'
9
+ await fs_fread(fp, Buffer.alloc(1024), 6, 5); // <Buffer 'world'>
10
+ await fs_fread(fp, Buffer.alloc(1024), 100); // <Buffer > (length 0)
11
+ ```
@@ -0,0 +1,2 @@
1
+ Resolves with the size in bytes of an open file descriptor (`fs_fstat(fp).size`).
2
+ For a pathname, see `fs_size` / `fs_size_enoent`.
@@ -0,0 +1,2 @@
1
+ Promisified `fs.fstat(fp, options)` — resolves with the `Stats` of an open file descriptor.
2
+ For pathnames, see `fs_stat` (follows symlinks) and `fs_lstat` (does not).
package/src/fs_fwrite.js CHANGED
@@ -5,12 +5,12 @@ const fs = require('fs');
5
5
  */
6
6
  async function fs_fwrite(fp, buffer, offset = null)
7
7
  {
8
- const {bytesWritten} = await new Promise(function (resolve, reject) {
8
+ const bytes_written = await new Promise(function (resolve, reject) {
9
9
  fs.write(fp, buffer, 0, buffer.length, offset, function (error, out) {
10
10
  error ? reject(error) : resolve(out);
11
11
  });
12
12
  });
13
- return bytesWritten;
13
+ return bytes_written;
14
14
  }
15
15
 
16
16
  module.exports = fs_fwrite;
@@ -0,0 +1,9 @@
1
+ Writes the whole `buffer` to a file descriptor and resolves with `bytes_written`.
2
+ With `offset = null` (default) it writes at the current file position;
3
+ pass an explicit `offset` to write at that position instead.
4
+
5
+ ```js
6
+ const fp = await fs_fopen(file, 'w');
7
+ await fs_fwrite(fp, Buffer.from('hello world')); // 11
8
+ await fs_fwrite(fp, Buffer.from('WORLD'), 6); // 5 — file is now 'hello WORLD'
9
+ ```
package/src/fs_list.md ADDED
@@ -0,0 +1,5 @@
1
+ Shallow directory listing: resolves with an array of `fs_fi` info objects for the direct
2
+ entries of `pathname` (the directory itself is not included).
3
+ A non-directory pathname yields `[fi]` of itself.
4
+ Entry pathnames are `pathname` joined with each name, so they stay relative if the input is relative.
5
+ See `fs_list_deep` and `fs_find` for recursive variants.
@@ -0,0 +1,5 @@
1
+ Recursively lists `path` as an array of `fs_fi` info objects, including the root entry
2
+ itself and every directory below it.
3
+ Traversal is stack-based: the root comes first, but the rest is in no particular order.
4
+ Pathnames are joined from the input, so they stay relative if the input is relative.
5
+ See `fs_list` (shallow) and `fs_find` (recursive, absolute pathnames, depth-first order).
package/src/fs_ls.md ADDED
@@ -0,0 +1,2 @@
1
+ **Deprecated** in favor of `fs_list` (identical behavior):
2
+ a shallow directory listing as an array of `fs_fi` info objects.
package/src/fs_lsr.md ADDED
@@ -0,0 +1,4 @@
1
+ **Deprecated** in favor of `fs_walk`.
2
+ Recursive listing like `fs_find`, but keeps pathnames relative to the input and adds
3
+ `content_size` to every `fs_fi` entry: a file's own `size`, the recursive total of file
4
+ sizes for a directory, and `0` for anything else.
@@ -0,0 +1,3 @@
1
+ Thin wrapper over `fs.promises.lstat(path, options)`: stats the link itself instead of
2
+ following symlinks.
3
+ See `fs_stat` (follows symlinks) and `fs_fi` (lstat plus pathname/flags/symlink-target info).
@@ -0,0 +1,3 @@
1
+ Thin wrapper over `fs.promises.mkdir(path, options)`.
2
+ Rejects with `EEXIST` if the path exists, and does not create missing parents unless
3
+ `options.recursive` is set — see `fs_mkdirp` for that.
package/src/fs_mkdirp.js CHANGED
@@ -1,36 +1,9 @@
1
- const fs_exists = require('./fs_exists');
2
- const fs_mkdir = require('./fs_mkdir');
3
- const fs_path_dirname = require('./fs_path_dirname');
1
+ const fs = require('fs');
4
2
 
5
3
  async function fs_mkdirp(pathname)
6
4
  {
7
- const parents = [];
8
-
9
- for (let p = pathname; p && p !== '/'; p = fs_path_dirname(p)) {
10
- if (await fs_exists(p)) {
11
- break;
12
- }
13
- parents.push(p);
14
- }
15
-
16
- while (parents.length) {
17
- try {
18
- await fs_mkdir(parents.pop());
19
- }
20
- catch (error) {
21
- // Error: EEXIST: file already exists, mkdir [...]
22
- if (error.code === 'EEXIST') {
23
- // Exiting here is not correct. When two processes are running in parallel,
24
- // one tries to create `a/b/c` and another `a`. When second process creates
25
- // `a`, the first process will get `EEXIST` and stops (break) creating any
26
- // nested directories, which is not correct.
27
- // break;
28
- continue;
29
- }
30
- throw error;
31
- }
32
- }
33
-
5
+ // Unlike the native call, resolves with `pathname` (handy for chaining).
6
+ await fs.promises.mkdir(pathname, {recursive: true});
34
7
  return pathname;
35
8
  }
36
9
 
@@ -0,0 +1,7 @@
1
+ Creates a directory and any missing parents (`mkdir -p`). An already existing directory
2
+ is fine (safe to call in parallel), but a path that exists as a regular file rejects with `EEXIST`.
3
+ Unlike the native call, resolves with `pathname` itself — handy for chaining.
4
+
5
+ ```js
6
+ await fs_mkdirp('/tmp/a/b/c'); // '/tmp/a/b/c'
7
+ ```
@@ -0,0 +1,2 @@
1
+ Thin wrapper over node's `path.basename(p)`, kept for the consistent `fs_*`
2
+ naming. Returns the last portion of the path.