@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.
- package/dist/build/build.js +24 -1
- package/dist/build/jsenv_core_packages.js +67 -30
- package/dist/client/directory_listing/directory_listing.html +1 -0
- package/dist/start_build_server/jsenv_core_packages.js +53 -25
- package/dist/start_dev_server/jsenv_core_packages.js +67 -30
- package/dist/start_dev_server/start_dev_server.js +25 -2
- package/package.json +7 -7
- package/src/dev/start_dev_server.js +1 -1
- package/src/plugins/protocol_file/client/directory_listing.html +1 -0
- package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +24 -1
package/dist/build/build.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
5746
|
-
|
|
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:
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
5212
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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.
|
|
76
|
-
"@jsenv/js-module-fallback": "1.4.
|
|
77
|
-
"@jsenv/plugin-bundling": "2.10.
|
|
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.
|
|
80
|
-
"@jsenv/plugin-transpilation": "1.5.
|
|
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.
|
|
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
|
|
@@ -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
|
|
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) {
|