@jsenv/test 3.7.22 → 3.7.24

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.
@@ -9,7 +9,7 @@ import { cpuUsage, memoryUsage } from "node:process";
9
9
  import { stripVTControlCharacters } from "node:util";
10
10
  import crypto from "node:crypto";
11
11
  import { dirname } from "node:path";
12
- import { createServer } from "node:net";
12
+ import { createConnection, createServer } from "node:net";
13
13
  import { spawn, spawnSync, fork } from "node:child_process";
14
14
  import { availableParallelism, cpus, totalmem, release, freemem } from "node:os";
15
15
  import { SOURCEMAP, generateSourcemapDataUrl } from "@jsenv/sourcemap";
@@ -854,44 +854,53 @@ const TIME_DICTIONARY_EN = {
854
854
  second: { long: "second", plural: "seconds", short: "s" },
855
855
  joinDuration: (primary, remaining) => `${primary} and ${remaining}`,
856
856
  };
857
- const TIME_DICTIONARY_FR = {
858
- year: { long: "an", plural: "ans", short: "a" },
859
- month: { long: "mois", plural: "mois", short: "m" },
860
- week: { long: "semaine", plural: "semaines", short: "s" },
861
- day: { long: "jour", plural: "jours", short: "j" },
862
- hour: { long: "heure", plural: "heures", short: "h" },
863
- minute: { long: "minute", plural: "minutes", short: "m" },
864
- second: { long: "seconde", plural: "secondes", short: "s" },
865
- joinDuration: (primary, remaining) => `${primary} et ${remaining}`,
866
- };
867
857
 
858
+ /**
859
+ * Converts a duration in milliseconds into a human-readable string intended for display in
860
+ * CLI output — where readability matters more than precision.
861
+ *
862
+ * - Values below 1ms are displayed as "0 second". Sub-millisecond durations are not
863
+ * meaningful at human scale, and showing "0.0001 second" (or switching to a "millisecond"
864
+ * unit) would hurt readability. The chosen trade-off is to always use "second" as the
865
+ * smallest unit and accept the loss of precision for very small values.
866
+ * - Values below 1s are displayed in fractional seconds (e.g. "0.05 second").
867
+ * - Values are expressed using the two most significant units (e.g. "1 hour and 23 minutes").
868
+ * - Rounding never causes a value to display as the next unit boundary
869
+ * (e.g. 59_999ms → "59.9 seconds", never "60 seconds").
870
+ *
871
+ * @param {number} ms - Duration in milliseconds.
872
+ * @param {object} [options]
873
+ * @param {boolean} [options.short=false] - Use compact unit symbols (e.g. "1h and 23m").
874
+ * @param {boolean} [options.rounded=true] - Round the last displayed digit. When false, truncates instead.
875
+ * @param {number} [options.decimals] - Override the number of decimal places shown.
876
+ * @returns {string}
877
+ */
868
878
  const humanizeDuration = (
869
879
  ms,
870
880
  {
871
881
  short,
872
882
  rounded = true,
873
883
  decimals,
874
- lang = "en",
875
- timeDictionnary = lang === "fr" ? TIME_DICTIONARY_FR : TIME_DICTIONARY_EN,
884
+ timeDictionnary = TIME_DICTIONARY_EN,
876
885
  } = {},
877
886
  ) => {
878
- // ignore ms below meaningfulMs so that:
879
- // humanizeDuration(0.5) -> "0 second"
880
- // humanizeDuration(1.1) -> "0.001 second" (and not "0.0011 second")
881
- // This tool is meant to be read by humans and it would be barely readable to see
882
- // "0.0001 second" (stands for 0.1 millisecond)
883
- // yes we could return "0.1 millisecond" but we choosed consistency over precision
884
- // so that the prefered unit is "second" (and does not become millisecond when ms is super small)
885
887
  if (ms < 1) {
886
- return short
887
- ? `0${timeDictionnary.second.short}`
888
- : `0 ${timeDictionnary.second.long}`;
888
+ if (short) {
889
+ return `0${timeDictionnary.second.short}`;
890
+ }
891
+ return `0 ${timeDictionnary.second.long}`;
889
892
  }
890
893
  const { primary, remaining } = parseMs(ms);
891
894
  if (!remaining) {
895
+ const primaryUnitIndex = UNIT_KEYS.indexOf(primary.name);
896
+ const nextUnitName = UNIT_KEYS[primaryUnitIndex - 1];
897
+ const maxCount = nextUnitName
898
+ ? UNIT_MS[nextUnitName] / UNIT_MS[primary.name]
899
+ : null;
892
900
  return humanizeDurationUnit(primary, {
893
901
  decimals:
894
902
  decimals === undefined ? (primary.name === "second" ? 1 : 0) : decimals,
903
+ maxCount,
895
904
  short,
896
905
  rounded,
897
906
  timeDictionnary,
@@ -909,15 +918,23 @@ const humanizeDuration = (
909
918
  rounded,
910
919
  timeDictionnary,
911
920
  });
921
+ if (short) {
922
+ return `${primaryText}${remainingText}`;
923
+ }
912
924
  return timeDictionnary.joinDuration(primaryText, remainingText);
913
925
  };
914
926
  const humanizeDurationUnit = (
915
927
  unit,
916
- { decimals, short, rounded, timeDictionnary },
928
+ { decimals, maxCount, short, rounded, timeDictionnary },
917
929
  ) => {
918
- const count = rounded
930
+ let count = rounded
919
931
  ? setRoundedPrecision(unit.count, { decimals })
920
932
  : setPrecision(unit.count, { decimals });
933
+ if (maxCount !== null && maxCount !== undefined && count >= maxCount) {
934
+ // Prevent rounding up to the next unit boundary (e.g. 59.999s → 60s → cap to 59.9s)
935
+ const factor = Math.pow(10, decimals ?? 0);
936
+ count = Math.floor(unit.count * factor) / factor;
937
+ }
921
938
  const name = unit.name;
922
939
  if (short) {
923
940
  const unitText = timeDictionnary[name].short;
@@ -970,6 +987,17 @@ const parseMs = (ms) => {
970
987
  },
971
988
  };
972
989
  }
990
+ // When remaining rounds up to a full next-unit (e.g. 59.999s rounds to 60s = 1min),
991
+ // drop the remaining to avoid displaying "59 minutes and 60 seconds".
992
+ const remainingUnitMs = UNIT_MS[remainingUnitName];
993
+ const nextUnitMs = UNIT_MS[firstUnitName];
994
+ const maxRemainingCount = nextUnitMs / remainingUnitMs; // e.g. 60 for seconds-in-a-minute
995
+ // Cap remaining so it never rounds up to the next unit boundary
996
+ // (e.g. 59.5s stays as 59s instead of rounding to 60s = 1min)
997
+ const cappedRemainingCount =
998
+ remainingUnitCount >= maxRemainingCount - 1
999
+ ? maxRemainingCount - 1
1000
+ : remainingUnitCount;
973
1001
  // - 1 year and 1 month is great
974
1002
  return {
975
1003
  primary: {
@@ -978,7 +1006,7 @@ const parseMs = (ms) => {
978
1006
  },
979
1007
  remaining: {
980
1008
  name: remainingUnitName,
981
- count: remainingUnitCount,
1009
+ count: cappedRemainingCount,
982
1010
  },
983
1011
  };
984
1012
  };
@@ -4184,13 +4212,11 @@ const applyPackageResolve = (packageSpecifier, resolutionContext) => {
4184
4212
  if (packageSpecifier === "") {
4185
4213
  throw new Error("invalid module specifier");
4186
4214
  }
4187
- if (
4188
- conditions.includes("node") &&
4189
- isSpecifierForNodeBuiltin(packageSpecifier)
4190
- ) {
4215
+ // "node:" prefixed specifiers always resolve to node builtins
4216
+ if (packageSpecifier.startsWith("node:")) {
4191
4217
  return createResolutionResult({
4192
4218
  type: "node_builtin_specifier",
4193
- url: `node:${packageSpecifier}`,
4219
+ url: packageSpecifier,
4194
4220
  });
4195
4221
  }
4196
4222
  let { packageName, packageSubpath } = parsePackageSpecifier(packageSpecifier);
@@ -4250,6 +4276,17 @@ const applyPackageResolve = (packageSpecifier, resolutionContext) => {
4250
4276
  packageJson,
4251
4277
  });
4252
4278
  }
4279
+ // Bare builtin names (without "node:" prefix) are valid only if no local package found
4280
+ // Local packages always take priority over builtins with the same name
4281
+ if (
4282
+ conditions.includes("node") &&
4283
+ isSpecifierForNodeBuiltin(packageSpecifier)
4284
+ ) {
4285
+ return createResolutionResult({
4286
+ type: "node_builtin_specifier",
4287
+ url: `node:${packageSpecifier}`,
4288
+ });
4289
+ }
4253
4290
  throw createModuleNotFoundError(packageName, resolutionContext);
4254
4291
  };
4255
4292
 
@@ -7203,32 +7240,18 @@ const basicFetch = async (
7203
7240
  };
7204
7241
 
7205
7242
  const pingServer = async (url) => {
7206
- const server = createServer();
7207
7243
  const { hostname, port } = new URL(url);
7208
7244
 
7209
- try {
7210
- await new Promise((resolve, reject) => {
7211
- server.on("error", reject);
7212
- server.on("listening", () => {
7213
- resolve();
7214
- });
7215
- server.listen(port, hostname);
7245
+ return new Promise((resolve) => {
7246
+ const socket = createConnection({ hostname, port: Number(port) });
7247
+ socket.on("connect", () => {
7248
+ socket.destroy();
7249
+ resolve(true);
7250
+ });
7251
+ socket.on("error", () => {
7252
+ resolve(false);
7216
7253
  });
7217
- } catch (error) {
7218
- if (error && error.code === "EADDRINUSE") {
7219
- return true;
7220
- }
7221
- if (error && error.code === "EACCES") {
7222
- return true;
7223
- }
7224
- throw error;
7225
- }
7226
- await new Promise((resolve, reject) => {
7227
- server.on("error", reject);
7228
- server.on("close", resolve);
7229
- server.close();
7230
7254
  });
7231
- return false;
7232
7255
  };
7233
7256
 
7234
7257
  const startServerUsingCommand = async (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/test",
3
- "version": "3.7.22",
3
+ "version": "3.7.24",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@c88/v8-coverage": "0.1.1",
33
- "@jsenv/ast": "6.8.1",
34
- "@jsenv/plugin-supervisor": "1.8.2",
35
- "@jsenv/sourcemap": "1.3.17",
33
+ "@jsenv/ast": "6.8.3",
34
+ "@jsenv/plugin-supervisor": "1.8.4",
35
+ "@jsenv/sourcemap": "1.3.19",
36
36
  "he": "1.2.0",
37
37
  "istanbul-lib-coverage": "3.2.2",
38
38
  "istanbul-lib-instrument": "6.0.3",
@@ -1,30 +1,16 @@
1
- import { createServer } from "node:net";
1
+ import { createConnection } from "node:net";
2
2
 
3
3
  export const pingServer = async (url) => {
4
- const server = createServer();
5
4
  const { hostname, port } = new URL(url);
6
5
 
7
- try {
8
- await new Promise((resolve, reject) => {
9
- server.on("error", reject);
10
- server.on("listening", () => {
11
- resolve();
12
- });
13
- server.listen(port, hostname);
6
+ return new Promise((resolve) => {
7
+ const socket = createConnection({ hostname, port: Number(port) });
8
+ socket.on("connect", () => {
9
+ socket.destroy();
10
+ resolve(true);
11
+ });
12
+ socket.on("error", () => {
13
+ resolve(false);
14
14
  });
15
- } catch (error) {
16
- if (error && error.code === "EADDRINUSE") {
17
- return true;
18
- }
19
- if (error && error.code === "EACCES") {
20
- return true;
21
- }
22
- throw error;
23
- }
24
- await new Promise((resolve, reject) => {
25
- server.on("error", reject);
26
- server.on("close", resolve);
27
- server.close();
28
15
  });
29
- return false;
30
16
  };