@jsenv/core 41.2.7 → 41.2.9

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.
@@ -6411,7 +6411,30 @@ const jsenvPluginDirectoryListing = ({
6411
6411
  const { request, requestedUrl, mainFilePath, rootDirectoryUrl } =
6412
6412
  reference.ownerUrlInfo.context;
6413
6413
  if (!fsStat) {
6414
- if (!request || request.headers["sec-fetch-dest"] !== "document") {
6414
+ if (!request) {
6415
+ // no request we should not serve directoy listing
6416
+ return null;
6417
+ }
6418
+ const secFetchDest = request.headers["sec-fetch-dest"];
6419
+ if (secFetchDest && secFetchDest !== "document") {
6420
+ // we have sec fetch dest and it's not document so it's not a navigation request, we should not serve directory listing
6421
+ return null;
6422
+ }
6423
+ if (!secFetchDest) {
6424
+ // beware we might end up here when nav context is not trusted (http, ip url etc)
6425
+ // in that case we fallback to detecting if the request explicitly accepts html.
6426
+ // browsers navigating to a page send "text/html,..." explicitly; programmatic
6427
+ // fetch clients like Node.js send "*/*" which should NOT trigger directory listing.
6428
+ // We must NOT use pickContentType here because it matches "text/html" via the
6429
+ // "*/*" wildcard, causing programmatic fetches to receive the directory listing
6430
+ // HTML page (status 200) instead of a 404.
6431
+ const acceptHeader = request.headers.accept || "";
6432
+ if (!acceptHeader.includes("text/html")) {
6433
+ return null;
6434
+ }
6435
+ }
6436
+ // requestedUrl must be a proper file:// URL (no encoded slashes)
6437
+ if (requestedUrl.includes("%2F") || requestedUrl.includes("%2f")) {
6415
6438
  return null;
6416
6439
  }
6417
6440
  if (url !== requestedUrl) {
@@ -871,44 +871,53 @@ const TIME_DICTIONARY_EN = {
871
871
  second: { long: "second", plural: "seconds", short: "s" },
872
872
  joinDuration: (primary, remaining) => `${primary} and ${remaining}`,
873
873
  };
874
- const TIME_DICTIONARY_FR = {
875
- year: { long: "an", plural: "ans", short: "a" },
876
- month: { long: "mois", plural: "mois", short: "m" },
877
- week: { long: "semaine", plural: "semaines", short: "s" },
878
- day: { long: "jour", plural: "jours", short: "j" },
879
- hour: { long: "heure", plural: "heures", short: "h" },
880
- minute: { long: "minute", plural: "minutes", short: "m" },
881
- second: { long: "seconde", plural: "secondes", short: "s" },
882
- joinDuration: (primary, remaining) => `${primary} et ${remaining}`,
883
- };
884
874
 
875
+ /**
876
+ * Converts a duration in milliseconds into a human-readable string intended for display in
877
+ * CLI output — where readability matters more than precision.
878
+ *
879
+ * - Values below 1ms are displayed as "0 second". Sub-millisecond durations are not
880
+ * meaningful at human scale, and showing "0.0001 second" (or switching to a "millisecond"
881
+ * unit) would hurt readability. The chosen trade-off is to always use "second" as the
882
+ * smallest unit and accept the loss of precision for very small values.
883
+ * - Values below 1s are displayed in fractional seconds (e.g. "0.05 second").
884
+ * - Values are expressed using the two most significant units (e.g. "1 hour and 23 minutes").
885
+ * - Rounding never causes a value to display as the next unit boundary
886
+ * (e.g. 59_999ms → "59.9 seconds", never "60 seconds").
887
+ *
888
+ * @param {number} ms - Duration in milliseconds.
889
+ * @param {object} [options]
890
+ * @param {boolean} [options.short=false] - Use compact unit symbols (e.g. "1h and 23m").
891
+ * @param {boolean} [options.rounded=true] - Round the last displayed digit. When false, truncates instead.
892
+ * @param {number} [options.decimals] - Override the number of decimal places shown.
893
+ * @returns {string}
894
+ */
885
895
  const humanizeDuration = (
886
896
  ms,
887
897
  {
888
898
  short,
889
899
  rounded = true,
890
900
  decimals,
891
- lang = "en",
892
- timeDictionnary = lang === "fr" ? TIME_DICTIONARY_FR : TIME_DICTIONARY_EN,
901
+ timeDictionnary = TIME_DICTIONARY_EN,
893
902
  } = {},
894
903
  ) => {
895
- // ignore ms below meaningfulMs so that:
896
- // humanizeDuration(0.5) -> "0 second"
897
- // humanizeDuration(1.1) -> "0.001 second" (and not "0.0011 second")
898
- // This tool is meant to be read by humans and it would be barely readable to see
899
- // "0.0001 second" (stands for 0.1 millisecond)
900
- // yes we could return "0.1 millisecond" but we choosed consistency over precision
901
- // so that the prefered unit is "second" (and does not become millisecond when ms is super small)
902
904
  if (ms < 1) {
903
- return short
904
- ? `0${timeDictionnary.second.short}`
905
- : `0 ${timeDictionnary.second.long}`;
905
+ if (short) {
906
+ return `0${timeDictionnary.second.short}`;
907
+ }
908
+ return `0 ${timeDictionnary.second.long}`;
906
909
  }
907
910
  const { primary, remaining } = parseMs(ms);
908
911
  if (!remaining) {
912
+ const primaryUnitIndex = UNIT_KEYS.indexOf(primary.name);
913
+ const nextUnitName = UNIT_KEYS[primaryUnitIndex - 1];
914
+ const maxCount = nextUnitName
915
+ ? UNIT_MS[nextUnitName] / UNIT_MS[primary.name]
916
+ : null;
909
917
  return humanizeDurationUnit(primary, {
910
918
  decimals:
911
919
  decimals === undefined ? (primary.name === "second" ? 1 : 0) : decimals,
920
+ maxCount,
912
921
  short,
913
922
  rounded,
914
923
  timeDictionnary,
@@ -926,15 +935,23 @@ const humanizeDuration = (
926
935
  rounded,
927
936
  timeDictionnary,
928
937
  });
938
+ if (short) {
939
+ return `${primaryText}${remainingText}`;
940
+ }
929
941
  return timeDictionnary.joinDuration(primaryText, remainingText);
930
942
  };
931
943
  const humanizeDurationUnit = (
932
944
  unit,
933
- { decimals, short, rounded, timeDictionnary },
945
+ { decimals, maxCount, short, rounded, timeDictionnary },
934
946
  ) => {
935
- const count = rounded
947
+ let count = rounded
936
948
  ? setRoundedPrecision(unit.count, { decimals })
937
949
  : setPrecision(unit.count, { decimals });
950
+ if (maxCount !== null && maxCount !== undefined && count >= maxCount) {
951
+ // Prevent rounding up to the next unit boundary (e.g. 59.999s → 60s → cap to 59.9s)
952
+ const factor = Math.pow(10, decimals ?? 0);
953
+ count = Math.floor(unit.count * factor) / factor;
954
+ }
938
955
  const name = unit.name;
939
956
  if (short) {
940
957
  const unitText = timeDictionnary[name].short;
@@ -987,6 +1004,17 @@ const parseMs = (ms) => {
987
1004
  },
988
1005
  };
989
1006
  }
1007
+ // When remaining rounds up to a full next-unit (e.g. 59.999s rounds to 60s = 1min),
1008
+ // drop the remaining to avoid displaying "59 minutes and 60 seconds".
1009
+ const remainingUnitMs = UNIT_MS[remainingUnitName];
1010
+ const nextUnitMs = UNIT_MS[firstUnitName];
1011
+ const maxRemainingCount = nextUnitMs / remainingUnitMs; // e.g. 60 for seconds-in-a-minute
1012
+ // Cap remaining so it never rounds up to the next unit boundary
1013
+ // (e.g. 59.5s stays as 59s instead of rounding to 60s = 1min)
1014
+ const cappedRemainingCount =
1015
+ remainingUnitCount >= maxRemainingCount - 1
1016
+ ? maxRemainingCount - 1
1017
+ : remainingUnitCount;
990
1018
  // - 1 year and 1 month is great
991
1019
  return {
992
1020
  primary: {
@@ -995,7 +1023,7 @@ const parseMs = (ms) => {
995
1023
  },
996
1024
  remaining: {
997
1025
  name: remainingUnitName,
998
- count: remainingUnitCount,
1026
+ count: cappedRemainingCount,
999
1027
  },
1000
1028
  };
1001
1029
  };
@@ -5742,13 +5770,11 @@ const applyPackageResolve = (packageSpecifier, resolutionContext) => {
5742
5770
  if (packageSpecifier === "") {
5743
5771
  throw new Error("invalid module specifier");
5744
5772
  }
5745
- if (
5746
- conditions.includes("node") &&
5747
- isSpecifierForNodeBuiltin(packageSpecifier)
5748
- ) {
5773
+ // "node:" prefixed specifiers always resolve to node builtins
5774
+ if (packageSpecifier.startsWith("node:")) {
5749
5775
  return createResolutionResult({
5750
5776
  type: "node_builtin_specifier",
5751
- url: `node:${packageSpecifier}`,
5777
+ url: packageSpecifier,
5752
5778
  });
5753
5779
  }
5754
5780
  let { packageName, packageSubpath } = parsePackageSpecifier(packageSpecifier);
@@ -5808,6 +5834,17 @@ const applyPackageResolve = (packageSpecifier, resolutionContext) => {
5808
5834
  packageJson,
5809
5835
  });
5810
5836
  }
5837
+ // Bare builtin names (without "node:" prefix) are valid only if no local package found
5838
+ // Local packages always take priority over builtins with the same name
5839
+ if (
5840
+ conditions.includes("node") &&
5841
+ isSpecifierForNodeBuiltin(packageSpecifier)
5842
+ ) {
5843
+ return createResolutionResult({
5844
+ type: "node_builtin_specifier",
5845
+ url: `node:${packageSpecifier}`,
5846
+ });
5847
+ }
5811
5848
  throw createModuleNotFoundError(packageName, resolutionContext);
5812
5849
  };
5813
5850
 
@@ -5,6 +5,7 @@
5
5
  <meta charset="utf-8">
6
6
  <link rel="icon" href="data:,">
7
7
  <link rel="stylesheet" href="./css/directory_listing.css">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
9
  </head>
9
10
 
10
11
  <body data-theme="dark">
@@ -780,44 +780,53 @@ const TIME_DICTIONARY_EN = {
780
780
  second: { long: "second", plural: "seconds", short: "s" },
781
781
  joinDuration: (primary, remaining) => `${primary} and ${remaining}`,
782
782
  };
783
- const TIME_DICTIONARY_FR = {
784
- year: { long: "an", plural: "ans", short: "a" },
785
- month: { long: "mois", plural: "mois", short: "m" },
786
- week: { long: "semaine", plural: "semaines", short: "s" },
787
- day: { long: "jour", plural: "jours", short: "j" },
788
- hour: { long: "heure", plural: "heures", short: "h" },
789
- minute: { long: "minute", plural: "minutes", short: "m" },
790
- second: { long: "seconde", plural: "secondes", short: "s" },
791
- joinDuration: (primary, remaining) => `${primary} et ${remaining}`,
792
- };
793
783
 
784
+ /**
785
+ * Converts a duration in milliseconds into a human-readable string intended for display in
786
+ * CLI output — where readability matters more than precision.
787
+ *
788
+ * - Values below 1ms are displayed as "0 second". Sub-millisecond durations are not
789
+ * meaningful at human scale, and showing "0.0001 second" (or switching to a "millisecond"
790
+ * unit) would hurt readability. The chosen trade-off is to always use "second" as the
791
+ * smallest unit and accept the loss of precision for very small values.
792
+ * - Values below 1s are displayed in fractional seconds (e.g. "0.05 second").
793
+ * - Values are expressed using the two most significant units (e.g. "1 hour and 23 minutes").
794
+ * - Rounding never causes a value to display as the next unit boundary
795
+ * (e.g. 59_999ms → "59.9 seconds", never "60 seconds").
796
+ *
797
+ * @param {number} ms - Duration in milliseconds.
798
+ * @param {object} [options]
799
+ * @param {boolean} [options.short=false] - Use compact unit symbols (e.g. "1h and 23m").
800
+ * @param {boolean} [options.rounded=true] - Round the last displayed digit. When false, truncates instead.
801
+ * @param {number} [options.decimals] - Override the number of decimal places shown.
802
+ * @returns {string}
803
+ */
794
804
  const humanizeDuration = (
795
805
  ms,
796
806
  {
797
807
  short,
798
808
  rounded = true,
799
809
  decimals,
800
- lang = "en",
801
- timeDictionnary = lang === "fr" ? TIME_DICTIONARY_FR : TIME_DICTIONARY_EN,
810
+ timeDictionnary = TIME_DICTIONARY_EN,
802
811
  } = {},
803
812
  ) => {
804
- // ignore ms below meaningfulMs so that:
805
- // humanizeDuration(0.5) -> "0 second"
806
- // humanizeDuration(1.1) -> "0.001 second" (and not "0.0011 second")
807
- // This tool is meant to be read by humans and it would be barely readable to see
808
- // "0.0001 second" (stands for 0.1 millisecond)
809
- // yes we could return "0.1 millisecond" but we choosed consistency over precision
810
- // so that the prefered unit is "second" (and does not become millisecond when ms is super small)
811
813
  if (ms < 1) {
812
- return short
813
- ? `0${timeDictionnary.second.short}`
814
- : `0 ${timeDictionnary.second.long}`;
814
+ if (short) {
815
+ return `0${timeDictionnary.second.short}`;
816
+ }
817
+ return `0 ${timeDictionnary.second.long}`;
815
818
  }
816
819
  const { primary, remaining } = parseMs(ms);
817
820
  if (!remaining) {
821
+ const primaryUnitIndex = UNIT_KEYS.indexOf(primary.name);
822
+ const nextUnitName = UNIT_KEYS[primaryUnitIndex - 1];
823
+ const maxCount = nextUnitName
824
+ ? UNIT_MS[nextUnitName] / UNIT_MS[primary.name]
825
+ : null;
818
826
  return humanizeDurationUnit(primary, {
819
827
  decimals:
820
828
  decimals === undefined ? (primary.name === "second" ? 1 : 0) : decimals,
829
+ maxCount,
821
830
  short,
822
831
  rounded,
823
832
  timeDictionnary,
@@ -835,15 +844,23 @@ const humanizeDuration = (
835
844
  rounded,
836
845
  timeDictionnary,
837
846
  });
847
+ if (short) {
848
+ return `${primaryText}${remainingText}`;
849
+ }
838
850
  return timeDictionnary.joinDuration(primaryText, remainingText);
839
851
  };
840
852
  const humanizeDurationUnit = (
841
853
  unit,
842
- { decimals, short, rounded, timeDictionnary },
854
+ { decimals, maxCount, short, rounded, timeDictionnary },
843
855
  ) => {
844
- const count = rounded
856
+ let count = rounded
845
857
  ? setRoundedPrecision(unit.count, { decimals })
846
858
  : setPrecision(unit.count, { decimals });
859
+ if (maxCount !== null && maxCount !== undefined && count >= maxCount) {
860
+ // Prevent rounding up to the next unit boundary (e.g. 59.999s → 60s → cap to 59.9s)
861
+ const factor = Math.pow(10, decimals ?? 0);
862
+ count = Math.floor(unit.count * factor) / factor;
863
+ }
847
864
  const name = unit.name;
848
865
  if (short) {
849
866
  const unitText = timeDictionnary[name].short;
@@ -896,6 +913,17 @@ const parseMs = (ms) => {
896
913
  },
897
914
  };
898
915
  }
916
+ // When remaining rounds up to a full next-unit (e.g. 59.999s rounds to 60s = 1min),
917
+ // drop the remaining to avoid displaying "59 minutes and 60 seconds".
918
+ const remainingUnitMs = UNIT_MS[remainingUnitName];
919
+ const nextUnitMs = UNIT_MS[firstUnitName];
920
+ const maxRemainingCount = nextUnitMs / remainingUnitMs; // e.g. 60 for seconds-in-a-minute
921
+ // Cap remaining so it never rounds up to the next unit boundary
922
+ // (e.g. 59.5s stays as 59s instead of rounding to 60s = 1min)
923
+ const cappedRemainingCount =
924
+ remainingUnitCount >= maxRemainingCount - 1
925
+ ? maxRemainingCount - 1
926
+ : remainingUnitCount;
899
927
  // - 1 year and 1 month is great
900
928
  return {
901
929
  primary: {
@@ -904,7 +932,7 @@ const parseMs = (ms) => {
904
932
  },
905
933
  remaining: {
906
934
  name: remainingUnitName,
907
- count: remainingUnitCount,
935
+ count: cappedRemainingCount,
908
936
  },
909
937
  };
910
938
  };
@@ -379,44 +379,53 @@ const TIME_DICTIONARY_EN = {
379
379
  second: { long: "second", plural: "seconds", short: "s" },
380
380
  joinDuration: (primary, remaining) => `${primary} and ${remaining}`,
381
381
  };
382
- const TIME_DICTIONARY_FR = {
383
- year: { long: "an", plural: "ans", short: "a" },
384
- month: { long: "mois", plural: "mois", short: "m" },
385
- week: { long: "semaine", plural: "semaines", short: "s" },
386
- day: { long: "jour", plural: "jours", short: "j" },
387
- hour: { long: "heure", plural: "heures", short: "h" },
388
- minute: { long: "minute", plural: "minutes", short: "m" },
389
- second: { long: "seconde", plural: "secondes", short: "s" },
390
- joinDuration: (primary, remaining) => `${primary} et ${remaining}`,
391
- };
392
382
 
383
+ /**
384
+ * Converts a duration in milliseconds into a human-readable string intended for display in
385
+ * CLI output — where readability matters more than precision.
386
+ *
387
+ * - Values below 1ms are displayed as "0 second". Sub-millisecond durations are not
388
+ * meaningful at human scale, and showing "0.0001 second" (or switching to a "millisecond"
389
+ * unit) would hurt readability. The chosen trade-off is to always use "second" as the
390
+ * smallest unit and accept the loss of precision for very small values.
391
+ * - Values below 1s are displayed in fractional seconds (e.g. "0.05 second").
392
+ * - Values are expressed using the two most significant units (e.g. "1 hour and 23 minutes").
393
+ * - Rounding never causes a value to display as the next unit boundary
394
+ * (e.g. 59_999ms → "59.9 seconds", never "60 seconds").
395
+ *
396
+ * @param {number} ms - Duration in milliseconds.
397
+ * @param {object} [options]
398
+ * @param {boolean} [options.short=false] - Use compact unit symbols (e.g. "1h and 23m").
399
+ * @param {boolean} [options.rounded=true] - Round the last displayed digit. When false, truncates instead.
400
+ * @param {number} [options.decimals] - Override the number of decimal places shown.
401
+ * @returns {string}
402
+ */
393
403
  const humanizeDuration = (
394
404
  ms,
395
405
  {
396
406
  short,
397
407
  rounded = true,
398
408
  decimals,
399
- lang = "en",
400
- timeDictionnary = lang === "fr" ? TIME_DICTIONARY_FR : TIME_DICTIONARY_EN,
409
+ timeDictionnary = TIME_DICTIONARY_EN,
401
410
  } = {},
402
411
  ) => {
403
- // ignore ms below meaningfulMs so that:
404
- // humanizeDuration(0.5) -> "0 second"
405
- // humanizeDuration(1.1) -> "0.001 second" (and not "0.0011 second")
406
- // This tool is meant to be read by humans and it would be barely readable to see
407
- // "0.0001 second" (stands for 0.1 millisecond)
408
- // yes we could return "0.1 millisecond" but we choosed consistency over precision
409
- // so that the prefered unit is "second" (and does not become millisecond when ms is super small)
410
412
  if (ms < 1) {
411
- return short
412
- ? `0${timeDictionnary.second.short}`
413
- : `0 ${timeDictionnary.second.long}`;
413
+ if (short) {
414
+ return `0${timeDictionnary.second.short}`;
415
+ }
416
+ return `0 ${timeDictionnary.second.long}`;
414
417
  }
415
418
  const { primary, remaining } = parseMs(ms);
416
419
  if (!remaining) {
420
+ const primaryUnitIndex = UNIT_KEYS.indexOf(primary.name);
421
+ const nextUnitName = UNIT_KEYS[primaryUnitIndex - 1];
422
+ const maxCount = nextUnitName
423
+ ? UNIT_MS[nextUnitName] / UNIT_MS[primary.name]
424
+ : null;
417
425
  return humanizeDurationUnit(primary, {
418
426
  decimals:
419
427
  decimals === undefined ? (primary.name === "second" ? 1 : 0) : decimals,
428
+ maxCount,
420
429
  short,
421
430
  rounded,
422
431
  timeDictionnary,
@@ -434,15 +443,23 @@ const humanizeDuration = (
434
443
  rounded,
435
444
  timeDictionnary,
436
445
  });
446
+ if (short) {
447
+ return `${primaryText}${remainingText}`;
448
+ }
437
449
  return timeDictionnary.joinDuration(primaryText, remainingText);
438
450
  };
439
451
  const humanizeDurationUnit = (
440
452
  unit,
441
- { decimals, short, rounded, timeDictionnary },
453
+ { decimals, maxCount, short, rounded, timeDictionnary },
442
454
  ) => {
443
- const count = rounded
455
+ let count = rounded
444
456
  ? setRoundedPrecision(unit.count, { decimals })
445
457
  : setPrecision(unit.count, { decimals });
458
+ if (maxCount !== null && maxCount !== undefined && count >= maxCount) {
459
+ // Prevent rounding up to the next unit boundary (e.g. 59.999s → 60s → cap to 59.9s)
460
+ const factor = Math.pow(10, decimals ?? 0);
461
+ count = Math.floor(unit.count * factor) / factor;
462
+ }
446
463
  const name = unit.name;
447
464
  if (short) {
448
465
  const unitText = timeDictionnary[name].short;
@@ -495,6 +512,17 @@ const parseMs = (ms) => {
495
512
  },
496
513
  };
497
514
  }
515
+ // When remaining rounds up to a full next-unit (e.g. 59.999s rounds to 60s = 1min),
516
+ // drop the remaining to avoid displaying "59 minutes and 60 seconds".
517
+ const remainingUnitMs = UNIT_MS[remainingUnitName];
518
+ const nextUnitMs = UNIT_MS[firstUnitName];
519
+ const maxRemainingCount = nextUnitMs / remainingUnitMs; // e.g. 60 for seconds-in-a-minute
520
+ // Cap remaining so it never rounds up to the next unit boundary
521
+ // (e.g. 59.5s stays as 59s instead of rounding to 60s = 1min)
522
+ const cappedRemainingCount =
523
+ remainingUnitCount >= maxRemainingCount - 1
524
+ ? maxRemainingCount - 1
525
+ : remainingUnitCount;
498
526
  // - 1 year and 1 month is great
499
527
  return {
500
528
  primary: {
@@ -503,7 +531,7 @@ const parseMs = (ms) => {
503
531
  },
504
532
  remaining: {
505
533
  name: remainingUnitName,
506
- count: remainingUnitCount,
534
+ count: cappedRemainingCount,
507
535
  },
508
536
  };
509
537
  };
@@ -5208,13 +5236,11 @@ const applyPackageResolve = (packageSpecifier, resolutionContext) => {
5208
5236
  if (packageSpecifier === "") {
5209
5237
  throw new Error("invalid module specifier");
5210
5238
  }
5211
- if (
5212
- conditions.includes("node") &&
5213
- isSpecifierForNodeBuiltin(packageSpecifier)
5214
- ) {
5239
+ // "node:" prefixed specifiers always resolve to node builtins
5240
+ if (packageSpecifier.startsWith("node:")) {
5215
5241
  return createResolutionResult({
5216
5242
  type: "node_builtin_specifier",
5217
- url: `node:${packageSpecifier}`,
5243
+ url: packageSpecifier,
5218
5244
  });
5219
5245
  }
5220
5246
  let { packageName, packageSubpath } = parsePackageSpecifier(packageSpecifier);
@@ -5274,6 +5300,17 @@ const applyPackageResolve = (packageSpecifier, resolutionContext) => {
5274
5300
  packageJson,
5275
5301
  });
5276
5302
  }
5303
+ // Bare builtin names (without "node:" prefix) are valid only if no local package found
5304
+ // Local packages always take priority over builtins with the same name
5305
+ if (
5306
+ conditions.includes("node") &&
5307
+ isSpecifierForNodeBuiltin(packageSpecifier)
5308
+ ) {
5309
+ return createResolutionResult({
5310
+ type: "node_builtin_specifier",
5311
+ url: `node:${packageSpecifier}`,
5312
+ });
5313
+ }
5277
5314
  throw createModuleNotFoundError(packageName, resolutionContext);
5278
5315
  };
5279
5316
 
@@ -2471,7 +2471,30 @@ const jsenvPluginDirectoryListing = ({
2471
2471
  const { request, requestedUrl, mainFilePath, rootDirectoryUrl } =
2472
2472
  reference.ownerUrlInfo.context;
2473
2473
  if (!fsStat) {
2474
- if (!request || request.headers["sec-fetch-dest"] !== "document") {
2474
+ if (!request) {
2475
+ // no request we should not serve directoy listing
2476
+ return null;
2477
+ }
2478
+ const secFetchDest = request.headers["sec-fetch-dest"];
2479
+ if (secFetchDest && secFetchDest !== "document") {
2480
+ // we have sec fetch dest and it's not document so it's not a navigation request, we should not serve directory listing
2481
+ return null;
2482
+ }
2483
+ if (!secFetchDest) {
2484
+ // beware we might end up here when nav context is not trusted (http, ip url etc)
2485
+ // in that case we fallback to detecting if the request explicitly accepts html.
2486
+ // browsers navigating to a page send "text/html,..." explicitly; programmatic
2487
+ // fetch clients like Node.js send "*/*" which should NOT trigger directory listing.
2488
+ // We must NOT use pickContentType here because it matches "text/html" via the
2489
+ // "*/*" wildcard, causing programmatic fetches to receive the directory listing
2490
+ // HTML page (status 200) instead of a 404.
2491
+ const acceptHeader = request.headers.accept || "";
2492
+ if (!acceptHeader.includes("text/html")) {
2493
+ return null;
2494
+ }
2495
+ }
2496
+ // requestedUrl must be a proper file:// URL (no encoded slashes)
2497
+ if (requestedUrl.includes("%2F") || requestedUrl.includes("%2f")) {
2475
2498
  return null;
2476
2499
  }
2477
2500
  if (url !== requestedUrl) {
@@ -10148,7 +10171,7 @@ const startDevServer = async ({
10148
10171
  ignore,
10149
10172
  port = 3456,
10150
10173
  hostname,
10151
- acceptAnyIp,
10174
+ acceptAnyIp = true,
10152
10175
  https,
10153
10176
  // it's better to use http1 by default because it allows to get statusText in devtools
10154
10177
  // which gives valuable information when there is errors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "41.2.7",
3
+ "version": "41.2.9",
4
4
  "type": "module",
5
5
  "description": "Tool to develop, test and build js projects",
6
6
  "repository": {
@@ -72,14 +72,14 @@
72
72
  "test:snapshot_clear": "npx @jsenv/filesystem clear **/tests/**/side_effects/"
73
73
  },
74
74
  "dependencies": {
75
- "@jsenv/ast": "6.8.1",
76
- "@jsenv/js-module-fallback": "1.4.31",
77
- "@jsenv/plugin-bundling": "2.10.11",
75
+ "@jsenv/ast": "6.8.3",
76
+ "@jsenv/js-module-fallback": "1.4.33",
77
+ "@jsenv/plugin-bundling": "2.10.13",
78
78
  "@jsenv/plugin-minification": "1.7.3",
79
- "@jsenv/plugin-supervisor": "1.8.2",
80
- "@jsenv/plugin-transpilation": "1.5.73",
79
+ "@jsenv/plugin-supervisor": "1.8.4",
80
+ "@jsenv/plugin-transpilation": "1.5.75",
81
81
  "@jsenv/server": "17.3.0",
82
- "@jsenv/sourcemap": "1.3.17",
82
+ "@jsenv/sourcemap": "1.3.19",
83
83
  "react-table": "7.8.0"
84
84
  },
85
85
  "devDependencies": {
@@ -53,7 +53,7 @@ export const startDevServer = async ({
53
53
  ignore,
54
54
  port = 3456,
55
55
  hostname,
56
- acceptAnyIp,
56
+ acceptAnyIp = true,
57
57
  https,
58
58
  // it's better to use http1 by default because it allows to get statusText in devtools
59
59
  // which gives valuable information when there is errors
@@ -5,6 +5,7 @@
5
5
  <meta charset="utf-8" />
6
6
  <link rel="icon" href="data:," />
7
7
  <link rel="stylesheet" href="./directory_listing.css" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
9
  </head>
9
10
 
10
11
  <body data-theme="dark">
@@ -71,7 +71,30 @@ export const jsenvPluginDirectoryListing = ({
71
71
  const { request, requestedUrl, mainFilePath, rootDirectoryUrl } =
72
72
  reference.ownerUrlInfo.context;
73
73
  if (!fsStat) {
74
- if (!request || request.headers["sec-fetch-dest"] !== "document") {
74
+ if (!request) {
75
+ // no request we should not serve directoy listing
76
+ return null;
77
+ }
78
+ const secFetchDest = request.headers["sec-fetch-dest"];
79
+ if (secFetchDest && secFetchDest !== "document") {
80
+ // we have sec fetch dest and it's not document so it's not a navigation request, we should not serve directory listing
81
+ return null;
82
+ }
83
+ if (!secFetchDest) {
84
+ // beware we might end up here when nav context is not trusted (http, ip url etc)
85
+ // in that case we fallback to detecting if the request explicitly accepts html.
86
+ // browsers navigating to a page send "text/html,..." explicitly; programmatic
87
+ // fetch clients like Node.js send "*/*" which should NOT trigger directory listing.
88
+ // We must NOT use pickContentType here because it matches "text/html" via the
89
+ // "*/*" wildcard, causing programmatic fetches to receive the directory listing
90
+ // HTML page (status 200) instead of a 404.
91
+ const acceptHeader = request.headers.accept || "";
92
+ if (!acceptHeader.includes("text/html")) {
93
+ return null;
94
+ }
95
+ }
96
+ // requestedUrl must be a proper file:// URL (no encoded slashes)
97
+ if (requestedUrl.includes("%2F") || requestedUrl.includes("%2f")) {
75
98
  return null;
76
99
  }
77
100
  if (url !== requestedUrl) {