mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Compare commits
42 Commits
bun-v1.3.5
...
user/marko
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ec86dbe24 | ||
|
|
93b6ed3c1f | ||
|
|
dfed86e888 | ||
|
|
d4f678c6c7 | ||
|
|
ff8d32332f | ||
|
|
8eb39f3531 | ||
|
|
9451a8634e | ||
|
|
3336de6702 | ||
|
|
63a67e91a7 | ||
|
|
d81d16a055 | ||
|
|
2e0f9b8680 | ||
|
|
a357f11ea1 | ||
|
|
9c55050ee5 | ||
|
|
079aa68a94 | ||
|
|
fd1e31b5bb | ||
|
|
2ea6a71244 | ||
|
|
f15b358eee | ||
|
|
86802057fb | ||
|
|
83815e388d | ||
|
|
bace521b69 | ||
|
|
f0e6a89e59 | ||
|
|
086dc1c860 | ||
|
|
33e8e9bd50 | ||
|
|
2d892b5eb7 | ||
|
|
8b8c845708 | ||
|
|
f0be4443be | ||
|
|
0abed6e575 | ||
|
|
05c3a13366 | ||
|
|
1d95405da8 | ||
|
|
548a4f7859 | ||
|
|
709b8b9803 | ||
|
|
30f045dd41 | ||
|
|
6d1d14663b | ||
|
|
0ba7624824 | ||
|
|
f6d326d136 | ||
|
|
d85fd983f9 | ||
|
|
339a90ac1a | ||
|
|
33eac619ef | ||
|
|
b00bf53cb4 | ||
|
|
0972365627 | ||
|
|
bbf04de639 | ||
|
|
6bf431d707 |
@@ -18,6 +18,7 @@
|
||||
"fastify": "^5.0.0",
|
||||
"fdir": "^6.1.0",
|
||||
"mitata": "^1.0.25",
|
||||
"ms": "^4.0.0-nightly.202508271359",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"string-width": "7.1.0",
|
||||
@@ -363,7 +364,7 @@
|
||||
|
||||
"mitata": ["mitata@1.0.25", "", {}, "sha512-0v5qZtVW5vwj9FDvYfraR31BMDcRLkhSFWPTLaxx/Z3/EvScfVtAAWtMI2ArIbBcwh7P86dXh0lQWKiXQPlwYA=="],
|
||||
|
||||
"ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
||||
"ms": ["ms@4.0.0-nightly.202508271359", "", {}, "sha512-WC/Eo7NzFrOV/RRrTaI0fxKVbNCzEy76j2VqNV8SxDf9D69gSE2Lh0QwYvDlhiYmheBYExAvEAxVf5NoN0cj2A=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
@@ -497,6 +498,8 @@
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
||||
|
||||
"fastify/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
|
||||
|
||||
81
bench/ms/ms.mjs
Normal file
81
bench/ms/ms.mjs
Normal file
@@ -0,0 +1,81 @@
|
||||
import { ms } from "ms";
|
||||
import { bench, group, run } from "../runner.mjs";
|
||||
|
||||
const stringInputs = ["1s", "1m", "1h", "1d", "1w", "1y", "2 days", "10h", "2.5 hrs", "1.5h", "100ms"];
|
||||
const numberInputs = [1000, 60000, 3600000, 86400000, 604800000];
|
||||
|
||||
if (!process.argv.includes("--full")) {
|
||||
const str = ["1.5y"];
|
||||
if (typeof Bun === "undefined") {
|
||||
bench("ms (npm)", () => {
|
||||
ms(str[0]);
|
||||
});
|
||||
} else {
|
||||
bench("Bun.ms", () => {
|
||||
Bun.ms(str[0]);
|
||||
});
|
||||
bench("Bun.ms (statically inlined)", () => {
|
||||
Bun.ms("1.5y")
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (typeof Bun == "undefined" || process.argv.includes("--both")) {
|
||||
group("ms (npm)", () => {
|
||||
bench(`${stringInputs.length + numberInputs.length} inputs`, () => {
|
||||
for (const input of stringInputs) {
|
||||
ms(input);
|
||||
}
|
||||
for (const num of numberInputs) {
|
||||
ms(num);
|
||||
}
|
||||
});
|
||||
bench("string -> num", () => {
|
||||
ms(stringInputs[0]);
|
||||
});
|
||||
bench("num -> string", () => {
|
||||
ms(numberInputs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (typeof Bun != "undefined") {
|
||||
group("Bun.ms", () => {
|
||||
bench(`${stringInputs.length + numberInputs.length} inputs`, () => {
|
||||
for (const input of stringInputs) {
|
||||
Bun.ms(input);
|
||||
}
|
||||
for (const num of numberInputs) {
|
||||
Bun.ms(num);
|
||||
}
|
||||
});
|
||||
|
||||
bench("string -> num", () => {
|
||||
Bun.ms(stringInputs[0]);
|
||||
});
|
||||
bench("num -> string", () => {
|
||||
Bun.ms(numberInputs[0]);
|
||||
});
|
||||
|
||||
bench("statically inlined", () => {
|
||||
Bun.ms("1s");
|
||||
Bun.ms("1m");
|
||||
Bun.ms("1h");
|
||||
Bun.ms("1d");
|
||||
Bun.ms("1w");
|
||||
Bun.ms("1y");
|
||||
Bun.ms("2 days");
|
||||
Bun.ms("10h");
|
||||
Bun.ms("2.5 hrs");
|
||||
Bun.ms("1.5h");
|
||||
Bun.ms("100ms");
|
||||
|
||||
Bun.ms(1000);
|
||||
Bun.ms(60000);
|
||||
Bun.ms(3600000);
|
||||
Bun.ms(86400000);
|
||||
Bun.ms(604800000);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await run();
|
||||
@@ -15,6 +15,7 @@
|
||||
"fastify": "^5.0.0",
|
||||
"fdir": "^6.1.0",
|
||||
"mitata": "^1.0.25",
|
||||
"ms": "^4.0.0-nightly.202508271359",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"string-width": "7.1.0",
|
||||
|
||||
@@ -14,6 +14,4 @@ export function run(opts = {}) {
|
||||
|
||||
export const bench = Mitata.bench;
|
||||
|
||||
export function group(_name, fn) {
|
||||
return Mitata.group(fn);
|
||||
}
|
||||
export const group = Mitata.group;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[test]
|
||||
# Large monorepos (like Bun) may want to specify the test directory more specifically
|
||||
# Large monorepos (like Bun) may want to specify the test directory more specifically
|
||||
# By default, `bun test` scans every single folder recursively which, if you
|
||||
# have a gigantic submodule (like WebKit), requires lots of directory
|
||||
# traversals
|
||||
@@ -10,4 +10,4 @@ preload = "./test/preload.ts"
|
||||
|
||||
[install]
|
||||
linker = "isolated"
|
||||
minimumReleaseAge = 1
|
||||
minimumReleaseAge = 86400
|
||||
|
||||
@@ -449,6 +449,51 @@ namespace Bun {
|
||||
}
|
||||
```
|
||||
|
||||
## `Bun.ms`
|
||||
|
||||
Built-in alternative to the [`ms`](https://npmjs.com/package/ms) package. Convert between human-readable time strings and milliseconds. Automatically optimized at compile-time when bundling.
|
||||
|
||||
**Convert to Milliseconds**
|
||||
|
||||
```ts
|
||||
Bun.ms("2 days"); // 172800000
|
||||
Bun.ms("1d"); // 86400000
|
||||
Bun.ms("10h"); // 36000000
|
||||
Bun.ms("2.5 hrs"); // 9000000
|
||||
Bun.ms("2h"); // 7200000
|
||||
Bun.ms("1m"); // 60000
|
||||
Bun.ms("5s"); // 5000
|
||||
Bun.ms("1y"); // 31557600000
|
||||
Bun.ms("100"); // 100
|
||||
Bun.ms("-3 days"); // -259200000
|
||||
Bun.ms("-1h"); // -3600000
|
||||
Bun.ms("-200"); // -200
|
||||
```
|
||||
|
||||
**Convert from Milliseconds**
|
||||
|
||||
```ts
|
||||
Bun.ms(60000); // "1m"
|
||||
Bun.ms(2 * 60000); // "2m"
|
||||
Bun.ms(-3 * 60000); // "-3m"
|
||||
Bun.ms(Bun.ms("10 hours")); // "10h"
|
||||
|
||||
Bun.ms(60000, { long: true }); // "1 minute"
|
||||
Bun.ms(2 * 60000, { long: true }); // "2 minutes"
|
||||
Bun.ms(-3 * 60000, { long: true }); // "-3 minutes"
|
||||
Bun.ms(Bun.ms("10 hours"), { long: true }); // "10 hours"
|
||||
```
|
||||
|
||||
**Bundler Support**
|
||||
|
||||
```sh
|
||||
$ cat ./example.ts
|
||||
console.log(Bun.ms("2d"));
|
||||
|
||||
$ bun build ./example.ts --minify-syntax
|
||||
console.log(172800000);
|
||||
```
|
||||
|
||||
<!-- ## `Bun.enableANSIColors()` -->
|
||||
|
||||
## `Bun.fileURLToPath()`
|
||||
|
||||
@@ -223,19 +223,19 @@ For complete documentation refer to [Package manager > Global cache](https://bun
|
||||
|
||||
## Minimum release age
|
||||
|
||||
To protect against supply chain attacks where malicious packages are quickly published, you can configure a minimum age requirement for npm packages. Package versions published more recently than the specified threshold (in seconds) will be filtered out during installation.
|
||||
To protect against supply chain attacks where malicious packages are quickly published, you can configure a minimum age requirement for npm packages. Package versions published more recently than the specified threshold will be filtered out during installation.
|
||||
|
||||
```bash
|
||||
# Only install package versions published at least 3 days ago
|
||||
$ bun add @types/bun --minimum-release-age 259200 # seconds
|
||||
$ bun add @types/bun --minimum-release-age 3d
|
||||
```
|
||||
|
||||
You can also configure this in `bunfig.toml`:
|
||||
|
||||
```toml
|
||||
```toml#bunfig.toml
|
||||
[install]
|
||||
# Only install package versions published at least 3 days ago
|
||||
minimumReleaseAge = 259200 # seconds
|
||||
minimumReleaseAge = "3d"
|
||||
|
||||
# Exclude trusted packages from the age gate
|
||||
minimumReleaseAgeExcludes = ["@types/node", "typescript"]
|
||||
@@ -257,7 +257,7 @@ For more advanced security scanning, including integration with services & custo
|
||||
|
||||
The default behavior of `bun install` can be configured in `bunfig.toml`. The default values are shown below.
|
||||
|
||||
```toml
|
||||
```toml#bunfig.toml
|
||||
[install]
|
||||
|
||||
# whether to install optionalDependencies
|
||||
@@ -289,7 +289,7 @@ concurrentScripts = 16 # (cpu count or GOMAXPROCS) x2
|
||||
linker = "hoisted"
|
||||
|
||||
# minimum age config
|
||||
minimumReleaseAge = 259200 # seconds
|
||||
minimumReleaseAge = "3d"
|
||||
minimumReleaseAgeExcludes = ["@types/node", "typescript"]
|
||||
```
|
||||
|
||||
|
||||
@@ -612,12 +612,12 @@ Valid values are:
|
||||
|
||||
### `install.minimumReleaseAge`
|
||||
|
||||
Configure a minimum age (in seconds) for npm package versions. Package versions published more recently than this threshold will be filtered out during installation. Default is `null` (disabled).
|
||||
Configure a minimum age for npm package versions. Package versions published more recently than this threshold will be filtered out during installation. Default is `null` (disabled).
|
||||
|
||||
```toml
|
||||
[install]
|
||||
# Only install package versions published at least 3 days ago
|
||||
minimumReleaseAge = 259200
|
||||
minimumReleaseAge = "3d"
|
||||
# These packages will bypass the 3-day minimum age requirement
|
||||
minimumReleaseAgeExcludes = ["@types/bun", "typescript"]
|
||||
```
|
||||
|
||||
37
packages/bun-types/bun.d.ts
vendored
37
packages/bun-types/bun.d.ts
vendored
@@ -3753,6 +3753,43 @@ declare module "bun" {
|
||||
*/
|
||||
function sleepSync(ms: number): void;
|
||||
|
||||
/**
|
||||
* Parse a time string and return milliseconds, or format milliseconds as a string.
|
||||
*
|
||||
* Drop-in replacement for the `ms` npm package with compile-time inlining support.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.ms("2d") // 172800000
|
||||
* Bun.ms("1.5h") // 5400000
|
||||
* Bun.ms("1m") // 60000
|
||||
* Bun.ms("5s") // 5000
|
||||
* Bun.ms("100ms") // 100
|
||||
* Bun.ms("1mo") // 2629800000
|
||||
* Bun.ms("1y") // 31557600000
|
||||
* Bun.ms("100") // 100
|
||||
* Bun.ms(60000) // "1m"
|
||||
* Bun.ms(60000, { long: true }) // "1 minute"
|
||||
* Bun.ms("invalid") // NaN
|
||||
* ```
|
||||
*
|
||||
* Supports these units:
|
||||
* - `ms`, `millisecond`, `milliseconds`
|
||||
* - `s`, `sec`, `second`, `seconds`
|
||||
* - `m`, `min`, `minute`, `minutes`
|
||||
* - `h`, `hr`, `hour`, `hours`
|
||||
* - `d`, `day`, `days`
|
||||
* - `w`, `week`, `weeks`
|
||||
* - `mo`, `month`, `months`
|
||||
* - `y`, `yr`, `year`, `years`
|
||||
*
|
||||
* @param value - Time string to parse or milliseconds to format
|
||||
* @param options - Formatting options (when value is a number)
|
||||
* @returns Milliseconds (for string) or formatted string (for number). Returns NaN for invalid strings.
|
||||
*/
|
||||
function ms(value: string): number;
|
||||
function ms(value: number, options?: { long?: boolean }): string;
|
||||
|
||||
/**
|
||||
* Hash `input` using [SHA-2 512/256](https://en.wikipedia.org/wiki/SHA-2#Comparison_of_SHA_functions)
|
||||
*
|
||||
|
||||
@@ -2771,6 +2771,21 @@ pub fn NewParser_(
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime allow_macros) {
|
||||
// Track {ms} from "bun" as it will 99% of the time be statically known
|
||||
// so might as well make it a macro automatically
|
||||
if (strings.eqlComptime(path.text, "bun") and strings.eqlComptime(item.alias, "ms")) {
|
||||
try p.macro.refs.put(ref, .{
|
||||
.import_record_id = stmt.import_record_index,
|
||||
.name = item.alias,
|
||||
});
|
||||
const import_record = &p.import_records.items[stmt.import_record_index];
|
||||
if (import_record.tag == .none) {
|
||||
import_record.tag = .bun;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (macro_remap) |*remap| {
|
||||
if (remap.get(item.alias)) |remapped_path| {
|
||||
const new_import_id = p.addImportRecord(.stmt, path.loc, remapped_path);
|
||||
|
||||
@@ -1387,6 +1387,13 @@ pub fn VisitExpr(
|
||||
|
||||
const name = macro_ref_data.name orelse e_.target.data.e_dot.name;
|
||||
const record = &p.import_records.items[macro_ref_data.import_record_id];
|
||||
|
||||
// Special case: import { ms } from "bun" - inline instead of executing as macro
|
||||
if (record.tag == .bun and strings.eqlComptime(name, "ms")) {
|
||||
const res = bun.handleOom(bun.api.ms.astFunction(p, e_, expr.loc));
|
||||
return res orelse expr;
|
||||
}
|
||||
|
||||
const copied = Expr{ .loc = expr.loc, .data = .{ .e_call = e_ } };
|
||||
const start_error_count = p.log.msgs.items.len;
|
||||
p.macro_call_count += 1;
|
||||
@@ -1497,6 +1504,19 @@ pub fn VisitExpr(
|
||||
}
|
||||
};
|
||||
|
||||
// Constant folding for Bun.ms("1s") -> 1000 and Bun.ms(1000) -> "1s"
|
||||
if (p.should_fold_typescript_constant_expressions or p.options.features.inlining) {
|
||||
if (e_.target.data.as(.e_dot)) |dot| {
|
||||
if (dot.target.data == .e_identifier and strings.eqlComptime(dot.name, "ms")) {
|
||||
const symbol = &p.symbols.items[dot.target.data.e_identifier.ref.innerIndex()];
|
||||
if (symbol.kind == .unbound and strings.eqlComptime(symbol.original_name, "Bun")) {
|
||||
const res = bun.handleOom(bun.api.ms.astFunction(p, e_, expr.loc));
|
||||
if (res) |r| return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
pub fn e_new(p: *P, expr: Expr, _: ExprIn) Expr {
|
||||
|
||||
@@ -51,6 +51,7 @@ pub const UDPSocket = @import("./api/bun/udp_socket.zig").UDPSocket;
|
||||
pub const Valkey = @import("../valkey/js_valkey.zig").JSValkeyClient;
|
||||
pub const BlockList = @import("./node/net/BlockList.zig");
|
||||
pub const NativeZstd = @import("./node/zlib/NativeZstd.zig");
|
||||
pub const ms = @import("./api/bun/ms.zig");
|
||||
|
||||
pub const napi = @import("../napi/napi.zig");
|
||||
pub const node = @import("./node.zig");
|
||||
|
||||
@@ -25,6 +25,7 @@ pub const BunObject = struct {
|
||||
pub const jest = toJSCallback(@import("../test/jest.zig").Jest.call);
|
||||
pub const listen = toJSCallback(host_fn.wrapStaticMethod(api.Listener, "listen", false));
|
||||
pub const mmap = toJSCallback(Bun.mmapFile);
|
||||
pub const ms = toJSCallback(api.ms.jsFunction);
|
||||
pub const nanoseconds = toJSCallback(Bun.nanoseconds);
|
||||
pub const openInEditor = toJSCallback(Bun.openInEditor);
|
||||
pub const registerMacro = toJSCallback(Bun.registerMacro);
|
||||
@@ -161,6 +162,7 @@ pub const BunObject = struct {
|
||||
@export(&BunObject.jest, .{ .name = callbackName("jest") });
|
||||
@export(&BunObject.listen, .{ .name = callbackName("listen") });
|
||||
@export(&BunObject.mmap, .{ .name = callbackName("mmap") });
|
||||
@export(&BunObject.ms, .{ .name = callbackName("ms") });
|
||||
@export(&BunObject.nanoseconds, .{ .name = callbackName("nanoseconds") });
|
||||
@export(&BunObject.openInEditor, .{ .name = callbackName("openInEditor") });
|
||||
@export(&BunObject.registerMacro, .{ .name = callbackName("registerMacro") });
|
||||
|
||||
308
src/bun.js/api/bun/ms.zig
Normal file
308
src/bun.js/api/bun/ms.zig
Normal file
@@ -0,0 +1,308 @@
|
||||
/// Parse a time string like "2d", "1.5h", "5m" to milliseconds
|
||||
pub fn parse(input: []const u8) ?f64 {
|
||||
if (input.len == 0 or input.len > 100) return null;
|
||||
|
||||
var i: usize = 0;
|
||||
|
||||
next: switch (input[i]) {
|
||||
'-', '.', '0'...'9' => {
|
||||
i += 1;
|
||||
if (i < input.len) continue :next input[i];
|
||||
break :next;
|
||||
},
|
||||
' ', 'a'...'z', 'A'...'Z' => break :next,
|
||||
else => return null,
|
||||
}
|
||||
|
||||
const value = std.fmt.parseFloat(f64, input[0..i]) catch return null;
|
||||
|
||||
const unit = strings.trimLeadingChar(input[i..], ' ');
|
||||
if (unit.len == 0) return value;
|
||||
|
||||
if (MultiplierMap.getASCIIICaseInsensitive(unit)) |m| {
|
||||
return value * m;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Years (365.25 days to account for leap years)
|
||||
// (matching the `ms` package implementation)
|
||||
const ms_per_year = std.time.ms_per_day * 365.25;
|
||||
const ms_per_month = std.time.ms_per_day * (365.25 / 12.0);
|
||||
|
||||
const MultiplierMap = bun.ComptimeStringMap(f64, .{
|
||||
// Years (365.25 days to account for leap years)
|
||||
.{ "y", ms_per_year },
|
||||
.{ "yr", ms_per_year },
|
||||
.{ "yrs", ms_per_year },
|
||||
.{ "year", ms_per_year },
|
||||
.{ "years", ms_per_year },
|
||||
|
||||
// Months (30.4375 days average)
|
||||
.{ "mo", ms_per_month },
|
||||
.{ "month", ms_per_month },
|
||||
.{ "months", ms_per_month },
|
||||
|
||||
// Weeks
|
||||
.{ "w", std.time.ms_per_week },
|
||||
.{ "week", std.time.ms_per_week },
|
||||
.{ "weeks", std.time.ms_per_week },
|
||||
|
||||
// Days
|
||||
.{ "d", std.time.ms_per_day },
|
||||
.{ "day", std.time.ms_per_day },
|
||||
.{ "days", std.time.ms_per_day },
|
||||
|
||||
// Hours
|
||||
.{ "h", std.time.ms_per_hour },
|
||||
.{ "hr", std.time.ms_per_hour },
|
||||
.{ "hrs", std.time.ms_per_hour },
|
||||
.{ "hour", std.time.ms_per_hour },
|
||||
.{ "hours", std.time.ms_per_hour },
|
||||
|
||||
// Minutes
|
||||
.{ "m", std.time.ms_per_min },
|
||||
.{ "min", std.time.ms_per_min },
|
||||
.{ "mins", std.time.ms_per_min },
|
||||
.{ "minute", std.time.ms_per_min },
|
||||
.{ "minutes", std.time.ms_per_min },
|
||||
|
||||
// Seconds
|
||||
.{ "s", std.time.ms_per_s },
|
||||
.{ "sec", std.time.ms_per_s },
|
||||
.{ "secs", std.time.ms_per_s },
|
||||
.{ "second", std.time.ms_per_s },
|
||||
.{ "seconds", std.time.ms_per_s },
|
||||
|
||||
// Milliseconds
|
||||
.{ "ms", 1 },
|
||||
.{ "msec", 1 },
|
||||
.{ "msecs", 1 },
|
||||
.{ "millisecond", 1 },
|
||||
.{ "milliseconds", 1 },
|
||||
});
|
||||
|
||||
// To keep the behavior consistent with JavaScript, we can't use @round
|
||||
// Zig's @round uses "round half away from zero": ties round away from zero (2.5→3, -2.5→-3)
|
||||
// JavaScript's Math.round uses "round half toward +∞": ties round toward positive infinity (2.5→3, -2.5→-2)
|
||||
fn jsMathRound(x: f64) i64 {
|
||||
const i: f64 = @ceil(x);
|
||||
if ((i - 0.5) > x) return @intFromFloat(i - 1.0);
|
||||
return @intFromFloat(i);
|
||||
}
|
||||
|
||||
/// Format milliseconds to a human-readable string
|
||||
pub fn format(allocator: std.mem.Allocator, ms: f64, long: bool) ![]u8 {
|
||||
const abs_ms = @abs(ms);
|
||||
|
||||
// Years
|
||||
if (abs_ms >= ms_per_year) {
|
||||
const years = jsMathRound(ms / ms_per_year);
|
||||
if (long) {
|
||||
const plural = abs_ms >= ms_per_year * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} year{s}", .{ years, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}y", .{years});
|
||||
}
|
||||
|
||||
// Months
|
||||
if (abs_ms >= ms_per_month) {
|
||||
const months = jsMathRound(ms / ms_per_month);
|
||||
if (long) {
|
||||
const plural = abs_ms >= ms_per_month * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} month{s}", .{ months, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}mo", .{months});
|
||||
}
|
||||
|
||||
// Weeks
|
||||
if (abs_ms >= std.time.ms_per_week) {
|
||||
const weeks = jsMathRound(ms / std.time.ms_per_week);
|
||||
if (long) {
|
||||
const plural = abs_ms >= std.time.ms_per_week * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} week{s}", .{ weeks, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}w", .{weeks});
|
||||
}
|
||||
|
||||
// Days
|
||||
if (abs_ms >= std.time.ms_per_day) {
|
||||
const days = jsMathRound(ms / std.time.ms_per_day);
|
||||
if (long) {
|
||||
const plural = abs_ms >= std.time.ms_per_day * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} day{s}", .{ days, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}d", .{days});
|
||||
}
|
||||
|
||||
// Hours
|
||||
if (abs_ms >= std.time.ms_per_hour) {
|
||||
const hours = jsMathRound(ms / std.time.ms_per_hour);
|
||||
if (long) {
|
||||
const plural = abs_ms >= std.time.ms_per_hour * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} hour{s}", .{ hours, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}h", .{hours});
|
||||
}
|
||||
|
||||
// Minutes
|
||||
if (abs_ms >= std.time.ms_per_min) {
|
||||
const minutes = jsMathRound(ms / std.time.ms_per_min);
|
||||
if (long) {
|
||||
const plural = abs_ms >= std.time.ms_per_min * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} minute{s}", .{ minutes, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}m", .{minutes});
|
||||
}
|
||||
|
||||
// Seconds
|
||||
if (abs_ms >= std.time.ms_per_s) {
|
||||
const seconds = jsMathRound(ms / std.time.ms_per_s);
|
||||
if (long) {
|
||||
const plural = abs_ms >= std.time.ms_per_s * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} second{s}", .{ seconds, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}s", .{seconds});
|
||||
}
|
||||
|
||||
// Milliseconds
|
||||
const ms_int: i64 = @intFromFloat(ms);
|
||||
if (long) {
|
||||
return std.fmt.allocPrint(allocator, "{d} ms", .{ms_int});
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}ms", .{ms_int});
|
||||
}
|
||||
|
||||
// Same as other format ms, but this is long and doesn't round
|
||||
// `ms` package doesn't have this, but it's more useful for some internal bun things
|
||||
pub fn formatLong(allocator: std.mem.Allocator, ms: f64) ![]u8 {
|
||||
const abs_ms = @abs(ms);
|
||||
|
||||
// Years
|
||||
if (abs_ms >= ms_per_year) {
|
||||
const years = abs_ms / ms_per_year;
|
||||
return std.fmt.allocPrint(allocator, "{d} year{s}", .{ years, if (years != 1) "s" else "" });
|
||||
}
|
||||
|
||||
// Months
|
||||
if (abs_ms >= ms_per_month) {
|
||||
const months = abs_ms / ms_per_month;
|
||||
return std.fmt.allocPrint(allocator, "{d} month{s}", .{ months, if (months != 1) "s" else "" });
|
||||
}
|
||||
|
||||
// Weeks
|
||||
if (abs_ms >= std.time.ms_per_week) {
|
||||
const weeks = abs_ms / std.time.ms_per_week;
|
||||
return std.fmt.allocPrint(allocator, "{d} week{s}", .{ weeks, if (weeks != 1) "s" else "" });
|
||||
}
|
||||
|
||||
// Days
|
||||
if (abs_ms >= std.time.ms_per_day) {
|
||||
const days = abs_ms / std.time.ms_per_day;
|
||||
return std.fmt.allocPrint(allocator, "{d} day{s}", .{ days, if (days != 1) "s" else "" });
|
||||
}
|
||||
|
||||
// Hours
|
||||
if (abs_ms >= std.time.ms_per_hour) {
|
||||
const hours = abs_ms / std.time.ms_per_hour;
|
||||
return std.fmt.allocPrint(allocator, "{d} hour{s}", .{ hours, if (hours != 1) "s" else "" });
|
||||
}
|
||||
|
||||
// Minutes
|
||||
if (abs_ms >= std.time.ms_per_min) {
|
||||
const minutes = abs_ms / std.time.ms_per_min;
|
||||
return std.fmt.allocPrint(allocator, "{d} minute{s}", .{ minutes, if (minutes != 1) "s" else "" });
|
||||
}
|
||||
|
||||
// Seconds
|
||||
if (abs_ms >= std.time.ms_per_s) {
|
||||
const seconds = abs_ms / std.time.ms_per_s;
|
||||
return std.fmt.allocPrint(allocator, "{d} second{s}", .{ seconds, if (seconds != 1) "s" else "" });
|
||||
}
|
||||
|
||||
// Milliseconds
|
||||
return std.fmt.allocPrint(allocator, "{d} ms", .{abs_ms});
|
||||
}
|
||||
|
||||
/// JavaScript function: Bun.ms(value, options?)
|
||||
pub fn jsFunction(
|
||||
globalThis: *JSGlobalObject,
|
||||
callframe: *jsc.CallFrame,
|
||||
) JSError!jsc.JSValue {
|
||||
const input, const options = callframe.argumentsAsArray(2);
|
||||
|
||||
// If input is a number, format it to a string
|
||||
if (input.isNumber()) {
|
||||
const ms_value = input.asNumber();
|
||||
|
||||
if (std.math.isNan(ms_value) or std.math.isInf(ms_value)) {
|
||||
return globalThis.throwInvalidArguments("Value must be a finite number", .{});
|
||||
}
|
||||
|
||||
var long = false;
|
||||
if (options.isObject()) {
|
||||
if (try options.get(globalThis, "long")) |long_value| {
|
||||
long = long_value.toBoolean();
|
||||
}
|
||||
}
|
||||
|
||||
const result = try format(bun.default_allocator, ms_value, long);
|
||||
|
||||
var str = String.createExternalGloballyAllocated(.latin1, result);
|
||||
return str.transferToJS(globalThis);
|
||||
}
|
||||
|
||||
// If input is a string, parse it to milliseconds
|
||||
if (input.isString()) {
|
||||
const str = try input.toSlice(globalThis, bun.default_allocator);
|
||||
defer str.deinit();
|
||||
|
||||
const result = parse(str.slice()) orelse std.math.nan(f64);
|
||||
return JSValue.jsNumber(result);
|
||||
}
|
||||
|
||||
return globalThis.throwInvalidArguments("Bun.ms() expects a string or number", .{});
|
||||
}
|
||||
|
||||
// Bundler macro inlining for Bun.ms
|
||||
pub fn astFunction(p: anytype, e_: *const E.Call, loc: logger.Loc) !?Expr {
|
||||
if (e_.args.len == 0) return null;
|
||||
const arg = e_.args.at(0).unwrapInlined();
|
||||
|
||||
if (arg.asString(p.allocator)) |str| {
|
||||
const ms_value = parse(str) orelse std.math.nan(f64);
|
||||
return p.newExpr(E.Number{ .value = ms_value }, loc);
|
||||
}
|
||||
|
||||
if (arg.asNumber()) |num| {
|
||||
if (std.math.isNan(num) or std.math.isInf(num)) return null;
|
||||
|
||||
var long = false;
|
||||
if (e_.args.len >= 2) {
|
||||
const opts = e_.args.at(1).unwrapInlined();
|
||||
if (opts.getBoolean("long")) |b| {
|
||||
long = b;
|
||||
}
|
||||
}
|
||||
|
||||
const formatted = try format(p.allocator, num, long);
|
||||
return p.newExpr(E.String.init(formatted), loc);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
const JSError = bun.JSError;
|
||||
const String = bun.String;
|
||||
const logger = bun.logger;
|
||||
const strings = bun.strings;
|
||||
|
||||
const E = bun.ast.E;
|
||||
const Expr = bun.ast.Expr;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const JSGlobalObject = jsc.JSGlobalObject;
|
||||
const JSValue = jsc.JSValue;
|
||||
@@ -55,6 +55,7 @@
|
||||
macro(jest) \
|
||||
macro(listen) \
|
||||
macro(mmap) \
|
||||
macro(ms) \
|
||||
macro(nanoseconds) \
|
||||
macro(openInEditor) \
|
||||
macro(registerMacro) \
|
||||
|
||||
@@ -759,6 +759,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
|
||||
udpSocket BunObject_callback_udpSocket DontDelete|Function 1
|
||||
main bunObjectMain DontDelete|CustomAccessor
|
||||
mmap BunObject_callback_mmap DontDelete|Function 1
|
||||
ms BunObject_callback_ms DontDelete|Function 2
|
||||
nanoseconds functionBunNanoseconds DontDelete|Function 0
|
||||
openInEditor BunObject_callback_openInEditor DontDelete|Function 1
|
||||
origin BunObject_lazyPropCb_wrap_origin DontEnum|ReadOnly|DontDelete|PropertyCallback
|
||||
|
||||
@@ -702,8 +702,23 @@ pub const Bunfig = struct {
|
||||
}
|
||||
install.minimum_release_age_ms = days.value * std.time.ms_per_s;
|
||||
},
|
||||
.e_string => |str| {
|
||||
const str_value = str.string(allocator) catch {
|
||||
try this.addError(min_age.loc, "Failed to parse minimumReleaseAge");
|
||||
return;
|
||||
};
|
||||
if (bun.api.ms.parse(str_value)) |ms| {
|
||||
if (ms < 0) {
|
||||
try this.addError(min_age.loc, "Expected positive age (e.g 3d, 24h) for minimumReleaseAge");
|
||||
return;
|
||||
}
|
||||
install.minimum_release_age_ms = ms;
|
||||
} else {
|
||||
try this.addError(min_age.loc, "Expected positive age (e.g 3d, 24h) for minimumReleaseAge");
|
||||
}
|
||||
},
|
||||
else => {
|
||||
try this.addError(min_age.loc, "Expected number of seconds for minimumReleaseAge");
|
||||
try this.addError(min_age.loc, "Expected positive age (e.g 3d, 24h) for minimumReleaseAge");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ const shared_params = [_]ParamType{
|
||||
clap.parseParam("--omit <dev|optional|peer>... Exclude 'dev', 'optional', or 'peer' dependencies from install") catch unreachable,
|
||||
clap.parseParam("--lockfile-only Generate a lockfile without installing dependencies") catch unreachable,
|
||||
clap.parseParam("--linker <STR> Linker strategy (one of \"isolated\" or \"hoisted\")") catch unreachable,
|
||||
clap.parseParam("--minimum-release-age <NUM> Only install packages published at least N seconds ago (security feature)") catch unreachable,
|
||||
clap.parseParam("--minimum-release-age <STR> Only resolve package versions that are at least this old (e.g., 3d, 24h)") catch unreachable,
|
||||
clap.parseParam("--cpu <STR>... Override CPU architecture for optional dependencies (e.g., x64, arm64, * for all)") catch unreachable,
|
||||
clap.parseParam("--os <STR>... Override operating system for optional dependencies (e.g., linux, darwin, * for all)") catch unreachable,
|
||||
clap.parseParam("-h, --help Print this help menu") catch unreachable,
|
||||
@@ -836,13 +836,22 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com
|
||||
cli.save_text_lockfile = true;
|
||||
}
|
||||
|
||||
if (args.option("--minimum-release-age")) |min_age_secs| {
|
||||
if (args.option("--minimum-release-age")) |min_age_secs| brk: {
|
||||
const secs = std.fmt.parseFloat(f64, min_age_secs) catch {
|
||||
Output.errGeneric("Expected --minimum-release-age to be a positive number: {s}", .{min_age_secs});
|
||||
Global.crash();
|
||||
if (bun.api.ms.parse(min_age_secs)) |ms| {
|
||||
if (ms < 0) {
|
||||
Output.errGeneric("Expected --minimum-release-age to be a positive age (e.g 3d, 24h): {s}", .{min_age_secs});
|
||||
Global.crash();
|
||||
}
|
||||
cli.minimum_release_age_ms = ms;
|
||||
break :brk;
|
||||
} else {
|
||||
Output.errGeneric("Expected --minimum-release-age to be a positive number of seconds: {s}", .{min_age_secs});
|
||||
Global.crash();
|
||||
}
|
||||
};
|
||||
if (secs < 0) {
|
||||
Output.errGeneric("Expected --minimum-release-age to be a positive number: {s}", .{min_age_secs});
|
||||
Output.errGeneric("Expected --minimum-release-age to be a positive age (e.g 3d, 24h): {s}", .{min_age_secs});
|
||||
Global.crash();
|
||||
}
|
||||
cli.minimum_release_age_ms = secs * std.time.ms_per_s;
|
||||
|
||||
@@ -591,17 +591,19 @@ pub fn enqueueDependencyWithMainAndSuccessFn(
|
||||
err,
|
||||
);
|
||||
} else {
|
||||
const age_gate_ms = this.options.minimum_release_age_ms orelse 0;
|
||||
const age_gate = bun.handleOom(bun.api.ms.formatLong(this.allocator, this.options.minimum_release_age_ms orelse 0));
|
||||
defer this.allocator.free(age_gate);
|
||||
|
||||
if (version.tag == .dist_tag) {
|
||||
this.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
this.allocator,
|
||||
"Package \"{s}\" with tag \"{s}\" not found<r> <d>(all versions blocked by minimum-release-age: {d} seconds)<r>",
|
||||
"Package \"{s}\" with tag \"{s}\" not found<r> <d>(all versions blocked by minimum-release-age: {s})<r>",
|
||||
.{
|
||||
this.lockfile.str(&name),
|
||||
this.lockfile.str(&version.value.dist_tag.tag),
|
||||
age_gate_ms / std.time.ms_per_s,
|
||||
age_gate,
|
||||
},
|
||||
) catch unreachable;
|
||||
} else {
|
||||
@@ -609,11 +611,11 @@ pub fn enqueueDependencyWithMainAndSuccessFn(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
this.allocator,
|
||||
"No version matching \"{s}\" found for specifier \"{s}\"<r> <d>(blocked by minimum-release-age: {d} seconds)<r>",
|
||||
"No version matching \"{s}\" found for specifier \"{s}\"<r> <d>(blocked by minimum-release-age: {s})<r>",
|
||||
.{
|
||||
this.lockfile.str(&name),
|
||||
this.lockfile.str(&version.literal),
|
||||
age_gate_ms / std.time.ms_per_s,
|
||||
age_gate,
|
||||
},
|
||||
) catch unreachable;
|
||||
}
|
||||
@@ -1637,26 +1639,28 @@ fn getOrPutResolvedPackage(
|
||||
const package_name = this.lockfile.str(&name);
|
||||
if (this.options.log_level.isVerbose()) {
|
||||
if (filtered.newest_filtered) |newest| {
|
||||
const min_age_seconds = (this.options.minimum_release_age_ms orelse 0) / std.time.ms_per_s;
|
||||
const min_age_gate = bun.handleOom(bun.api.ms.formatLong(this.allocator, this.options.minimum_release_age_ms orelse 0));
|
||||
defer this.allocator.free(min_age_gate);
|
||||
|
||||
switch (version.tag) {
|
||||
.dist_tag => {
|
||||
const tag_str = this.lockfile.str(&version.value.dist_tag.tag);
|
||||
Output.prettyErrorln("<d>[minimum-release-age]<r> <b>{s}@{s}<r> selected <green>{s}<r> instead of <yellow>{s}<r> due to {d}-second filter", .{
|
||||
Output.prettyErrorln("<d>[minimum-release-age]<r> <b>{s}@{s}<r> selected <green>{s}<r> instead of <yellow>{s}<r> due to {s} filter", .{
|
||||
package_name,
|
||||
tag_str,
|
||||
filtered.result.version.fmt(manifest.string_buf),
|
||||
newest.fmt(manifest.string_buf),
|
||||
min_age_seconds,
|
||||
min_age_gate,
|
||||
});
|
||||
},
|
||||
.npm => {
|
||||
const version_str = version.value.npm.version.fmt(manifest.string_buf);
|
||||
Output.prettyErrorln("<d>[minimum-release-age]<r> <b>{s}<r>@{s}<r> selected <green>{s}<r> instead of <yellow>{s}<r> due to {d}-second filter", .{
|
||||
Output.prettyErrorln("<d>[minimum-release-age]<r> <b>{s}<r>@{s}<r> selected <green>{s}<r> instead of <yellow>{s}<r> due to {s} filter", .{
|
||||
package_name,
|
||||
version_str,
|
||||
filtered.result.version.fmt(manifest.string_buf),
|
||||
newest.fmt(manifest.string_buf),
|
||||
min_age_seconds,
|
||||
min_age_gate,
|
||||
});
|
||||
},
|
||||
else => unreachable,
|
||||
|
||||
@@ -828,7 +828,30 @@ describe("minimum-release-age", () => {
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
expect(exitCode).toBe(0);
|
||||
const lockfile = await Bun.file(`${dir}/bun.lock`).text();
|
||||
expect(lockfile).toContain("regular-package@2.1.0");
|
||||
expect(lockfile).not.toContain("regular-package@3.0.0");
|
||||
});
|
||||
|
||||
test("filters packages by minimum release age (numeric seconds form)", async () => {
|
||||
using dir = tempDir("basic-filter-seconds", {
|
||||
"package.json": JSON.stringify({
|
||||
dependencies: { "regular-package": "*" },
|
||||
}),
|
||||
".npmrc": `registry=${mockRegistryUrl}`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "432000" /* 5d */, "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -851,7 +874,7 @@ describe("minimum-release-age", () => {
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${9.5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "9.5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -877,7 +900,7 @@ describe("minimum-release-age", () => {
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -905,7 +928,7 @@ describe("minimum-release-age", () => {
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${1.8 * SECONDS_PER_DAY}`, "--verbose"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "1.8d", "--verbose"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -945,7 +968,7 @@ describe("minimum-release-age", () => {
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${1.8 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "1.8d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -984,7 +1007,7 @@ describe("minimum-release-age", () => {
|
||||
//
|
||||
// Result: Selects 1.0.6 (gave up finding stable version)
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1011,7 +1034,7 @@ describe("minimum-release-age", () => {
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${3 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "3d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1039,7 +1062,7 @@ describe("minimum-release-age", () => {
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${3 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "3d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1064,7 +1087,7 @@ describe("minimum-release-age", () => {
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${10 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "10d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1095,7 +1118,7 @@ describe("minimum-release-age", () => {
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${3 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "3d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1127,7 +1150,7 @@ describe("minimum-release-age", () => {
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${3 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "3d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1180,7 +1203,7 @@ describe("minimum-release-age", () => {
|
||||
// - 1.1.0 (15 days): PASSES age gate, but beyond search window (3 + 7 = 10 days)
|
||||
// - Should return 1.1.0 as best_version before breaking, not error!
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${3 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "3d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1209,7 +1232,7 @@ describe("minimum-release-age", () => {
|
||||
},
|
||||
}),
|
||||
"bunfig.toml": `[install]
|
||||
minimumReleaseAge = ${5 * SECONDS_PER_DAY}
|
||||
minimumReleaseAge = "5d"
|
||||
minimumReleaseAgeExcludes = ["excluded-package"]
|
||||
registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
@@ -1242,7 +1265,33 @@ registry = "${mockRegistryUrl}"`,
|
||||
dependencies: { "regular-package": "*" },
|
||||
}),
|
||||
"bunfig.toml": `[install]
|
||||
minimumReleaseAge = ${5 * SECONDS_PER_DAY}
|
||||
minimumReleaseAge = "5d"
|
||||
registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const lockfile = await Bun.file(`${dir}/bun.lock`).text();
|
||||
expect(lockfile).toContain("regular-package@2.1.0");
|
||||
expect(lockfile).not.toContain("regular-package@3.0.0");
|
||||
});
|
||||
|
||||
test("bunfig.toml configuration works (numeric seconds form)", async () => {
|
||||
using dir = tempDir("bunfig-config-seconds", {
|
||||
"package.json": JSON.stringify({
|
||||
dependencies: { "regular-package": "*" },
|
||||
}),
|
||||
"bunfig.toml": `[install]
|
||||
minimumReleaseAge = 432000 # 5d
|
||||
registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
@@ -1268,13 +1317,13 @@ registry = "${mockRegistryUrl}"`,
|
||||
dependencies: { "regular-package": "*" },
|
||||
}),
|
||||
"bunfig.toml": `[install]
|
||||
minimumReleaseAge = ${10 * SECONDS_PER_DAY}
|
||||
minimumReleaseAge = "10d"
|
||||
registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
// CLI says 5 days, bunfig says 10 days
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1297,7 +1346,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
dependencies: { "regular-package": "*" },
|
||||
}),
|
||||
"bunfig.toml": `[install]
|
||||
minimumReleaseAge = ${10 * SECONDS_PER_DAY}
|
||||
minimumReleaseAge = "10d"
|
||||
registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
@@ -1321,7 +1370,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
// Create a fake home directory with global bunfig
|
||||
using globalConfigDir = tempDir("global-config", {
|
||||
".bunfig.toml": `[install]
|
||||
minimumReleaseAge = ${5 * SECONDS_PER_DAY}
|
||||
minimumReleaseAge = "5d"
|
||||
registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
@@ -1357,7 +1406,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
// Create a fake home directory with global bunfig
|
||||
using globalConfigDir = tempDir("global-config-override", {
|
||||
".bunfig.toml": `[install]
|
||||
minimumReleaseAge = ${10 * SECONDS_PER_DAY}
|
||||
minimumReleaseAge = "10d"
|
||||
registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
@@ -1367,7 +1416,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
dependencies: { "regular-package": "*" },
|
||||
}),
|
||||
"bunfig.toml": `[install]
|
||||
minimumReleaseAge = ${5 * SECONDS_PER_DAY}
|
||||
minimumReleaseAge = "5d"
|
||||
registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
@@ -1407,7 +1456,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--verbose"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--verbose"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1440,7 +1489,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1471,7 +1520,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1497,12 +1546,12 @@ registry = "${mockRegistryUrl}"`,
|
||||
dependencies: { "regular-package": "*" },
|
||||
}),
|
||||
"bunfig.toml": `[install]
|
||||
minimumReleaseAge = ${5 * SECONDS_PER_DAY}
|
||||
minimumReleaseAge = "5d"
|
||||
registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1531,7 +1580,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1561,7 +1610,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--dry-run"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--dry-run"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1601,7 +1650,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
|
||||
// Now update with minimum-release-age
|
||||
proc = Bun.spawn({
|
||||
cmd: [bunExe(), "update", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "update", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1625,7 +1674,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
}),
|
||||
".npmrc": `registry=${mockRegistryUrl}`,
|
||||
"bunfig.toml": `[install]
|
||||
minimumReleaseAge = ${5 * SECONDS_PER_DAY}
|
||||
minimumReleaseAge = "5d"
|
||||
registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
@@ -1679,7 +1728,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1710,7 +1759,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1745,7 +1794,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1775,7 +1824,7 @@ registry = "${mockRegistryUrl}"`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -1942,7 +1991,7 @@ linker = "${linker}"
|
||||
// - stable-package (latest): 3.2.0 is 30 days old → select 3.2.0 (passes gate, is latest)
|
||||
// - stable-package (3.0.0): pinned to 3.0.0 (legacy workspace - no age check on exact versions)
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -2094,7 +2143,7 @@ linker = "${linker}"
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -2119,7 +2168,7 @@ linker = "${linker}"
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -2146,7 +2195,7 @@ linker = "${linker}"
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -2175,7 +2224,7 @@ linker = "${linker}"
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -2202,7 +2251,7 @@ linker = "${linker}"
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -2235,7 +2284,7 @@ linker = "${linker}"
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -2263,7 +2312,7 @@ linker = "${linker}"
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
@@ -2289,7 +2338,7 @@ linker = "${linker}"
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", `${5 * SECONDS_PER_DAY}`, "--no-verify"],
|
||||
cmd: [bunExe(), "install", "--minimum-release-age", "5d", "--no-verify"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
|
||||
65
test/integration/bun-types/fixture/ms.ts
Normal file
65
test/integration/bun-types/fixture/ms.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { expectType } from "./utilities";
|
||||
|
||||
// Test string literal autocomplete and return types
|
||||
expectType(Bun.ms("1s")).is<number>();
|
||||
expectType(Bun.ms("1m")).is<number>();
|
||||
expectType(Bun.ms("1h")).is<number>();
|
||||
expectType(Bun.ms("1d")).is<number>();
|
||||
expectType(Bun.ms("1w")).is<number>();
|
||||
expectType(Bun.ms("1mo")).is<number>();
|
||||
expectType(Bun.ms("1y")).is<number>();
|
||||
|
||||
// Test with all unit variations
|
||||
expectType(Bun.ms("1ms")).is<number>();
|
||||
expectType(Bun.ms("1millisecond")).is<number>();
|
||||
expectType(Bun.ms("1milliseconds")).is<number>();
|
||||
expectType(Bun.ms("1second")).is<number>();
|
||||
expectType(Bun.ms("1 second")).is<number>();
|
||||
expectType(Bun.ms("1 seconds")).is<number>();
|
||||
expectType(Bun.ms("1minute")).is<number>();
|
||||
expectType(Bun.ms("1 minute")).is<number>();
|
||||
expectType(Bun.ms("1hour")).is<number>();
|
||||
expectType(Bun.ms("1 hour")).is<number>();
|
||||
expectType(Bun.ms("1day")).is<number>();
|
||||
expectType(Bun.ms("1 day")).is<number>();
|
||||
expectType(Bun.ms("1week")).is<number>();
|
||||
expectType(Bun.ms("1 week")).is<number>();
|
||||
expectType(Bun.ms("1month")).is<number>();
|
||||
expectType(Bun.ms("1 month")).is<number>();
|
||||
expectType(Bun.ms("1year")).is<number>();
|
||||
expectType(Bun.ms("1 year")).is<number>();
|
||||
|
||||
// Test with decimals and negatives
|
||||
expectType(Bun.ms("1.5h")).is<number>();
|
||||
expectType(Bun.ms("-1s")).is<number>();
|
||||
expectType(Bun.ms(".5m")).is<number>();
|
||||
expectType(Bun.ms("-.5h")).is<number>();
|
||||
|
||||
// Test number input (formatting)
|
||||
expectType(Bun.ms(1000)).is<string>();
|
||||
expectType(Bun.ms(60000)).is<string>();
|
||||
expectType(Bun.ms(3600000)).is<string>();
|
||||
|
||||
// Test with options
|
||||
expectType(Bun.ms(1000, { long: true })).is<string>();
|
||||
expectType(Bun.ms(60000, { long: false })).is<string>();
|
||||
|
||||
// Test generic string input (for dynamic values)
|
||||
const dynamicString: string = "1s";
|
||||
expectType(Bun.ms(dynamicString)).is<number>();
|
||||
|
||||
// Should NOT accept options with string input
|
||||
// @ts-expect-error - options only valid with number input
|
||||
Bun.ms("1s", { long: true });
|
||||
|
||||
// Number with options should work
|
||||
const formatted = Bun.ms(1000, { long: true });
|
||||
expectType(formatted).is<string>();
|
||||
|
||||
// Options should be optional
|
||||
const shortFormat = Bun.ms(1000);
|
||||
expectType(shortFormat).is<string>();
|
||||
|
||||
// Test that invalid inputs still return number (NaN is a number)
|
||||
expectType(Bun.ms("invalid")).is<number>();
|
||||
expectType(Bun.ms("")).is<number>();
|
||||
436
test/js/bun/util/ms.test.ts
Normal file
436
test/js/bun/util/ms.test.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
describe("Bun.ms - parse (string to number)", () => {
|
||||
test("short strings", () => {
|
||||
const cases = [
|
||||
["100", 100],
|
||||
["1m", 60000],
|
||||
["1h", 3600000],
|
||||
["2d", 172800000],
|
||||
["3w", 1814400000],
|
||||
["1s", 1000],
|
||||
["100ms", 100],
|
||||
["1y", 31557600000],
|
||||
["1.5h", 5400000],
|
||||
["1 s", 1000],
|
||||
["-.5h", -1800000],
|
||||
["-1h", -3600000],
|
||||
["-200", -200],
|
||||
[".5ms", 0.5],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("long strings", () => {
|
||||
const cases = [
|
||||
["53 milliseconds", 53],
|
||||
["17 msecs", 17],
|
||||
["1 sec", 1000],
|
||||
["1 min", 60000],
|
||||
["1 hr", 3600000],
|
||||
["2 days", 172800000],
|
||||
["1 week", 604800000],
|
||||
["1 month", 2629800000],
|
||||
["1 year", 31557600000],
|
||||
["1.5 hours", 5400000],
|
||||
["-100 milliseconds", -100],
|
||||
["-1.5 hours", -5400000],
|
||||
["-10 minutes", -600000],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("case insensitive", () => {
|
||||
const cases = [
|
||||
["1M", 60000],
|
||||
["1H", 3600000],
|
||||
["2D", 172800000],
|
||||
["3W", 1814400000],
|
||||
["1S", 1000],
|
||||
["1MS", 1],
|
||||
["1Y", 31557600000],
|
||||
["1 HOUR", 3600000],
|
||||
["1 DAY", 86400000],
|
||||
["1 WEEK", 604800000],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("invalid inputs", () => {
|
||||
const cases = [
|
||||
["", "empty string"],
|
||||
[" ", "whitespace only"],
|
||||
["foo", "invalid unit"],
|
||||
["1x", "unknown unit"],
|
||||
["1.2.3s", "multiple dots"],
|
||||
] as const;
|
||||
|
||||
for (const [input] of cases) {
|
||||
expect(Bun.ms(input)).toBeNaN();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bun.ms - format (number to string)", () => {
|
||||
test("short format", () => {
|
||||
const cases = [
|
||||
[0, "0ms"],
|
||||
[500, "500ms"],
|
||||
[-500, "-500ms"],
|
||||
[1000, "1s"],
|
||||
[10000, "10s"],
|
||||
[60000, "1m"],
|
||||
[600000, "10m"],
|
||||
[3600000, "1h"],
|
||||
[86400000, "1d"],
|
||||
[604800000, "1w"],
|
||||
[2629800000, "1mo"],
|
||||
[31557600001, "1y"],
|
||||
[234234234, "3d"],
|
||||
[-234234234, "-3d"],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("long format", () => {
|
||||
const cases = [
|
||||
[0, "0 ms"],
|
||||
[500, "500 ms"],
|
||||
[-500, "-500 ms"],
|
||||
[1000, "1 second"],
|
||||
[1001, "1 second"],
|
||||
[1499, "1 second"],
|
||||
[1500, "2 seconds"],
|
||||
[10000, "10 seconds"],
|
||||
[60000, "1 minute"],
|
||||
[600000, "10 minutes"],
|
||||
[3600000, "1 hour"],
|
||||
[86400000, "1 day"],
|
||||
[172800000, "2 days"],
|
||||
[604800000, "1 week"],
|
||||
[2629800000, "1 month"],
|
||||
[31557600001, "1 year"],
|
||||
[234234234, "3 days"],
|
||||
[-234234234, "-3 days"],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input, { long: true })).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("rounding behavior matches JavaScript Math.round and npm ms", () => {
|
||||
// JavaScript Math.round uses "round half toward +∞"
|
||||
// Positive ties (X.5) round up (away from zero): 2.5 → 3
|
||||
// Negative ties (X.5) round up toward zero: -2.5 → -2
|
||||
// This is different from Zig's @round which rounds away from zero
|
||||
// (so we made our own jsMathRound function)
|
||||
const cases = [
|
||||
// Positive ties - should round up
|
||||
[1000, "1s", "1 second"],
|
||||
[1500, "2s", "2 seconds"],
|
||||
[2500, "3s", "3 seconds"],
|
||||
[3500, "4s", "4 seconds"],
|
||||
[4500, "5s", "5 seconds"],
|
||||
|
||||
// Negative ties - should round toward zero (toward +∞)
|
||||
[-1000, "-1s", "-1 second"],
|
||||
[-1500, "-1s", "-1 seconds"],
|
||||
[-2500, "-2s", "-2 seconds"],
|
||||
[-3500, "-3s", "-3 seconds"],
|
||||
[-4500, "-4s", "-4 seconds"],
|
||||
|
||||
[9000000, "3h", "3 hours"],
|
||||
[-9000000, "-2h", "-2 hours"],
|
||||
|
||||
[216000000, "3d", "3 days"],
|
||||
[-216000000, "-2d", "-2 days"],
|
||||
] as const;
|
||||
|
||||
for (const [input, expectedShort, expectedLong] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expectedShort);
|
||||
expect(Bun.ms(input, { long: true })).toBe(expectedLong);
|
||||
}
|
||||
});
|
||||
|
||||
test("invalid number inputs", () => {
|
||||
expect(() => Bun.ms(NaN)).toThrow();
|
||||
expect(() => Bun.ms(Infinity)).toThrow();
|
||||
expect(() => Bun.ms(-Infinity)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bun.ms - comprehensive coverage", () => {
|
||||
test("all time units", () => {
|
||||
const cases = [
|
||||
// Milliseconds
|
||||
["1ms", 1],
|
||||
["1millisecond", 1],
|
||||
["1milliseconds", 1],
|
||||
["1msec", 1],
|
||||
["1msecs", 1],
|
||||
// Seconds
|
||||
["1s", 1000],
|
||||
["1sec", 1000],
|
||||
["1secs", 1000],
|
||||
["1second", 1000],
|
||||
["1seconds", 1000],
|
||||
["2seconds", 2000],
|
||||
// Minutes
|
||||
["1m", 60000],
|
||||
["1min", 60000],
|
||||
["1mins", 60000],
|
||||
["1minute", 60000],
|
||||
["1minutes", 60000],
|
||||
["2minutes", 120000],
|
||||
// Hours
|
||||
["1h", 3600000],
|
||||
["1hr", 3600000],
|
||||
["1hrs", 3600000],
|
||||
["1hour", 3600000],
|
||||
["1hours", 3600000],
|
||||
["2hours", 7200000],
|
||||
// Days
|
||||
["1d", 86400000],
|
||||
["1day", 86400000],
|
||||
["1days", 86400000],
|
||||
["2days", 172800000],
|
||||
// Weeks
|
||||
["1w", 604800000],
|
||||
["1week", 604800000],
|
||||
["1weeks", 604800000],
|
||||
["2weeks", 1209600000],
|
||||
// Months
|
||||
["1mo", 2629800000],
|
||||
["1month", 2629800000],
|
||||
["1months", 2629800000],
|
||||
["2months", 5259600000],
|
||||
// Years
|
||||
["1y", 31557600000],
|
||||
["1yr", 31557600000],
|
||||
["1yrs", 31557600000],
|
||||
["1year", 31557600000],
|
||||
["1years", 31557600000],
|
||||
["2years", 63115200000],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("decimals and negatives", () => {
|
||||
const cases = [
|
||||
["1.5s", 1500],
|
||||
["1.5h", 5400000],
|
||||
["0.5d", 43200000],
|
||||
["-1s", -1000],
|
||||
["-1.5h", -5400000],
|
||||
["-0.5d", -43200000],
|
||||
[".5s", 500],
|
||||
["-.5s", -500],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("whitespace handling", () => {
|
||||
const cases = [
|
||||
["1 s", 1000],
|
||||
["1 s", 1000],
|
||||
["1 s", 1000],
|
||||
[" 1s", NaN],
|
||||
["1s ", NaN],
|
||||
[" 1s ", NaN],
|
||||
["1 second", 1000],
|
||||
["1 seconds", 1000],
|
||||
[" 1 second ", NaN],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("Bun.ms - dynamic values at runtime", () => {
|
||||
{
|
||||
function getNumber() {
|
||||
return Math.random() > 0.5 ? 1 : 2;
|
||||
}
|
||||
const days = getNumber();
|
||||
const result = Bun.ms(days + "days");
|
||||
|
||||
// Should be either 1 day or 2 days
|
||||
expect(result === 86400000 || result === 172800000).toBe(true);
|
||||
}
|
||||
|
||||
{
|
||||
function getHours() {
|
||||
return 5;
|
||||
}
|
||||
const result = Bun.ms(String(getHours()) + "h");
|
||||
expect(result).toBe(18000000); // 5 hours
|
||||
}
|
||||
|
||||
{
|
||||
const timeStr = "10m";
|
||||
const result = Bun.ms(timeStr);
|
||||
expect(result).toBe(600000);
|
||||
}
|
||||
|
||||
{
|
||||
function getMs() {
|
||||
return 60000;
|
||||
}
|
||||
const result = Bun.ms(getMs());
|
||||
expect(result).toBe("1m");
|
||||
}
|
||||
});
|
||||
|
||||
test("Bun.ms - static string formatting", () => {
|
||||
expect(Bun.ms("5s")).toBe(5000);
|
||||
expect(Bun.ms(5000, { long: true })).toBe("5 seconds");
|
||||
expect(Bun.ms(5000, { long: false })).toBe("5s");
|
||||
});
|
||||
|
||||
test("Bun.ms - bundler output", async () => {
|
||||
const dir = tempDirWithFiles("ms-bundler", {
|
||||
"entry.ts": `
|
||||
const dynamic = () => Math.random() > 0.5 ? 1 : 2;
|
||||
|
||||
export const values = {
|
||||
// Valid strings - should inline to numbers
|
||||
oneSecond: Bun.ms("1s"),
|
||||
oneMinute: Bun.ms("1m"),
|
||||
oneHour: Bun.ms("1h"),
|
||||
oneDay: Bun.ms("1d"),
|
||||
twoWeeks: Bun.ms("2w"),
|
||||
halfYear: Bun.ms("0.5y"),
|
||||
withSpaces: Bun.ms("5 minutes"),
|
||||
negative: Bun.ms("-10s"),
|
||||
decimal: Bun.ms("1.5h"),
|
||||
justNumber: Bun.ms("100"),
|
||||
caseInsensitive: Bun.ms("2D"),
|
||||
|
||||
// Invalid strings - should inline to NaN
|
||||
invalid: Bun.ms("invalid"),
|
||||
empty: Bun.ms(""),
|
||||
|
||||
// Number inputs - should inline to strings
|
||||
formatShort: Bun.ms(1000),
|
||||
formatLong: Bun.ms(60000, { long: true }),
|
||||
|
||||
// dynamic should not inline
|
||||
dynamic: Bun.ms(\`\$\{dynamic()\}s\`),
|
||||
|
||||
// test
|
||||
dontBeWeird: abc.ms("1s"),
|
||||
};
|
||||
`,
|
||||
"bun.ts": `
|
||||
import { ms, sleep } from "bun";
|
||||
|
||||
const dynamic = () => Math.random() > 0.5 ? 1 : 2;
|
||||
|
||||
export const values = {
|
||||
import: ms("1s"),
|
||||
importLong: ms(1000, { long: true }),
|
||||
ms: Bun.ms(),
|
||||
mss: ms,
|
||||
sleep: sleep,
|
||||
dynamic: ms(\`\${dynamic()}s\`),
|
||||
dontBeWeird: abc.ms("1s"),
|
||||
};
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [join(dir, "entry.ts")],
|
||||
minify: {
|
||||
syntax: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
let output = await result.outputs[0].text();
|
||||
output = output.replace(/\/\/.*?\/entry\.ts/, "// entry.ts");
|
||||
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
"// entry.ts
|
||||
var dynamic = () => Math.random() > 0.5 ? 1 : 2, values = {
|
||||
oneSecond: 1000,
|
||||
oneMinute: 60000,
|
||||
oneHour: 3600000,
|
||||
oneDay: 86400000,
|
||||
twoWeeks: 1209600000,
|
||||
halfYear: 15778800000,
|
||||
withSpaces: 300000,
|
||||
negative: -1e4,
|
||||
decimal: 5400000,
|
||||
justNumber: 100,
|
||||
caseInsensitive: 172800000,
|
||||
invalid: NaN,
|
||||
empty: NaN,
|
||||
formatShort: "1s",
|
||||
formatLong: "1 minute",
|
||||
dynamic: Bun.ms(\`\${dynamic()}s\`),
|
||||
dontBeWeird: abc.ms("1s")
|
||||
};
|
||||
export {
|
||||
values
|
||||
};
|
||||
"
|
||||
`);
|
||||
|
||||
const bunResult = await Bun.build({
|
||||
entrypoints: [join(dir, "bun.ts")],
|
||||
minify: {
|
||||
syntax: true,
|
||||
},
|
||||
target: "bun",
|
||||
});
|
||||
|
||||
expect(bunResult.success).toBe(true);
|
||||
expect(bunResult.outputs).toHaveLength(1);
|
||||
|
||||
let bunOutput = await bunResult.outputs[0].text();
|
||||
bunOutput = bunOutput.replace(/\/\/.*?\/bun\.ts/, "// bun.ts");
|
||||
|
||||
expect(bunOutput).toMatchInlineSnapshot(`
|
||||
"// @bun
|
||||
// bun.ts
|
||||
var {ms, sleep } = globalThis.Bun;
|
||||
var dynamic = () => Math.random() > 0.5 ? 1 : 2, values = {
|
||||
import: 1000,
|
||||
importLong: "1 second",
|
||||
ms: Bun.ms(),
|
||||
mss: ms,
|
||||
sleep,
|
||||
dynamic: ms(\`\${dynamic()}s\`),
|
||||
dontBeWeird: abc.ms("1s")
|
||||
};
|
||||
export {
|
||||
values
|
||||
};
|
||||
"
|
||||
`);
|
||||
});
|
||||
367
test/js/bun/util/vercel-ms.test.ts
Normal file
367
test/js/bun/util/vercel-ms.test.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
import { ms } from "bun";
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
describe("ms(string)", () => {
|
||||
it("should not throw an error", () => {
|
||||
expect(() => {
|
||||
ms("1m");
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should preserve ms", () => {
|
||||
expect(ms("100")).toBe(100);
|
||||
});
|
||||
|
||||
it("should convert from m to ms", () => {
|
||||
expect(ms("1m")).toBe(60000);
|
||||
});
|
||||
|
||||
it("should convert from h to ms", () => {
|
||||
expect(ms("1h")).toBe(3600000);
|
||||
});
|
||||
|
||||
it("should convert d to ms", () => {
|
||||
expect(ms("2d")).toBe(172800000);
|
||||
});
|
||||
|
||||
it("should convert w to ms", () => {
|
||||
expect(ms("3w")).toBe(1814400000);
|
||||
});
|
||||
|
||||
it("should convert s to ms", () => {
|
||||
expect(ms("1s")).toBe(1000);
|
||||
});
|
||||
|
||||
it("should convert ms to ms", () => {
|
||||
expect(ms("100ms")).toBe(100);
|
||||
});
|
||||
|
||||
it("should convert y to ms", () => {
|
||||
expect(ms("1y")).toBe(31557600000);
|
||||
});
|
||||
|
||||
it("should work with decimals", () => {
|
||||
expect(ms("1.5h")).toBe(5400000);
|
||||
});
|
||||
|
||||
it("should work with multiple spaces", () => {
|
||||
expect(ms("1 s")).toBe(1000);
|
||||
});
|
||||
|
||||
it("should return NaN if invalid", () => {
|
||||
// @ts-expect-error - We expect this to fail.
|
||||
expect(Number.isNaN(ms("☃"))).toBe(true);
|
||||
// @ts-expect-error - We expect this to fail.
|
||||
expect(Number.isNaN(ms("10-.5"))).toBe(true);
|
||||
// @ts-expect-error - We expect this to fail.
|
||||
expect(Number.isNaN(ms("ms"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should be case-insensitive", () => {
|
||||
expect(ms("1.5H")).toBe(5400000);
|
||||
});
|
||||
|
||||
it("should work with numbers starting with .", () => {
|
||||
expect(ms(".5ms")).toBe(0.5);
|
||||
});
|
||||
|
||||
it("should work with negative integers", () => {
|
||||
expect(ms("-100ms")).toBe(-100);
|
||||
});
|
||||
|
||||
it("should work with negative decimals", () => {
|
||||
expect(ms("-1.5h")).toBe(-5400000);
|
||||
expect(ms("-10.5h")).toBe(-37800000);
|
||||
});
|
||||
|
||||
it('should work with negative decimals starting with "."', () => {
|
||||
expect(ms("-.5h")).toBe(-1800000);
|
||||
});
|
||||
});
|
||||
|
||||
// long strings
|
||||
|
||||
describe("ms(long string)", () => {
|
||||
it("should not throw an error", () => {
|
||||
expect(() => {
|
||||
ms("53 milliseconds");
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should convert milliseconds to ms", () => {
|
||||
expect(ms("53 milliseconds")).toBe(53);
|
||||
});
|
||||
|
||||
it("should convert msecs to ms", () => {
|
||||
expect(ms("17 msecs")).toBe(17);
|
||||
});
|
||||
|
||||
it("should convert sec to ms", () => {
|
||||
expect(ms("1 sec")).toBe(1000);
|
||||
});
|
||||
|
||||
it("should convert from min to ms", () => {
|
||||
expect(ms("1 min")).toBe(60000);
|
||||
});
|
||||
|
||||
it("should convert from hr to ms", () => {
|
||||
expect(ms("1 hr")).toBe(3600000);
|
||||
});
|
||||
|
||||
it("should convert days to ms", () => {
|
||||
expect(ms("2 days")).toBe(172800000);
|
||||
});
|
||||
|
||||
it("should convert weeks to ms", () => {
|
||||
expect(ms("1 week")).toBe(604800000);
|
||||
});
|
||||
|
||||
it("should convert years to ms", () => {
|
||||
expect(ms("1 year")).toBe(31557600000);
|
||||
});
|
||||
|
||||
it("should work with decimals", () => {
|
||||
expect(ms("1.5 hours")).toBe(5400000);
|
||||
});
|
||||
|
||||
it("should work with negative integers", () => {
|
||||
expect(ms("-100 milliseconds")).toBe(-100);
|
||||
});
|
||||
|
||||
it("should work with negative decimals", () => {
|
||||
expect(ms("-1.5 hours")).toBe(-5400000);
|
||||
});
|
||||
|
||||
it('should work with negative decimals starting with "."', () => {
|
||||
expect(ms("-.5 hr")).toBe(-1800000);
|
||||
});
|
||||
});
|
||||
|
||||
// numbers
|
||||
|
||||
describe("ms(number, { long: true })", () => {
|
||||
it("should not throw an error", () => {
|
||||
expect(() => {
|
||||
ms(500, { long: true });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should support milliseconds", () => {
|
||||
expect(ms(500, { long: true })).toBe("500 ms");
|
||||
|
||||
expect(ms(-500, { long: true })).toBe("-500 ms");
|
||||
});
|
||||
|
||||
it("should support seconds", () => {
|
||||
expect(ms(1000, { long: true })).toBe("1 second");
|
||||
expect(ms(1200, { long: true })).toBe("1 second");
|
||||
expect(ms(10000, { long: true })).toBe("10 seconds");
|
||||
|
||||
expect(ms(-1000, { long: true })).toBe("-1 second");
|
||||
expect(ms(-1200, { long: true })).toBe("-1 second");
|
||||
expect(ms(-10000, { long: true })).toBe("-10 seconds");
|
||||
});
|
||||
|
||||
it("should support minutes", () => {
|
||||
expect(ms(60 * 1000, { long: true })).toBe("1 minute");
|
||||
expect(ms(60 * 1200, { long: true })).toBe("1 minute");
|
||||
expect(ms(60 * 10000, { long: true })).toBe("10 minutes");
|
||||
|
||||
expect(ms(-1 * 60 * 1000, { long: true })).toBe("-1 minute");
|
||||
expect(ms(-1 * 60 * 1200, { long: true })).toBe("-1 minute");
|
||||
expect(ms(-1 * 60 * 10000, { long: true })).toBe("-10 minutes");
|
||||
});
|
||||
|
||||
it("should support hours", () => {
|
||||
expect(ms(60 * 60 * 1000, { long: true })).toBe("1 hour");
|
||||
expect(ms(60 * 60 * 1200, { long: true })).toBe("1 hour");
|
||||
expect(ms(60 * 60 * 10000, { long: true })).toBe("10 hours");
|
||||
|
||||
expect(ms(-1 * 60 * 60 * 1000, { long: true })).toBe("-1 hour");
|
||||
expect(ms(-1 * 60 * 60 * 1200, { long: true })).toBe("-1 hour");
|
||||
expect(ms(-1 * 60 * 60 * 10000, { long: true })).toBe("-10 hours");
|
||||
});
|
||||
|
||||
it("should support days", () => {
|
||||
expect(ms(1 * 24 * 60 * 60 * 1000, { long: true })).toBe("1 day");
|
||||
expect(ms(1 * 24 * 60 * 60 * 1200, { long: true })).toBe("1 day");
|
||||
expect(ms(6 * 24 * 60 * 60 * 1000, { long: true })).toBe("6 days");
|
||||
|
||||
expect(ms(-1 * 1 * 24 * 60 * 60 * 1000, { long: true })).toBe("-1 day");
|
||||
expect(ms(-1 * 1 * 24 * 60 * 60 * 1200, { long: true })).toBe("-1 day");
|
||||
expect(ms(-1 * 6 * 24 * 60 * 60 * 1000, { long: true })).toBe("-6 days");
|
||||
});
|
||||
|
||||
it("should support weeks", () => {
|
||||
expect(ms(1 * 7 * 24 * 60 * 60 * 1000, { long: true })).toBe("1 week");
|
||||
expect(ms(2 * 7 * 24 * 60 * 60 * 1000, { long: true })).toBe("2 weeks");
|
||||
|
||||
expect(ms(-1 * 1 * 7 * 24 * 60 * 60 * 1000, { long: true })).toBe("-1 week");
|
||||
expect(ms(-1 * 2 * 7 * 24 * 60 * 60 * 1000, { long: true })).toBe("-2 weeks");
|
||||
});
|
||||
|
||||
it("should support months", () => {
|
||||
expect(ms(30.4375 * 24 * 60 * 60 * 1000, { long: true })).toBe("1 month");
|
||||
expect(ms(30.4375 * 24 * 60 * 60 * 1200, { long: true })).toBe("1 month");
|
||||
expect(ms(30.4375 * 24 * 60 * 60 * 10000, { long: true })).toBe("10 months");
|
||||
|
||||
expect(ms(-1 * 30.4375 * 24 * 60 * 60 * 1000, { long: true })).toBe("-1 month");
|
||||
expect(ms(-1 * 30.4375 * 24 * 60 * 60 * 1200, { long: true })).toBe("-1 month");
|
||||
expect(ms(-1 * 30.4375 * 24 * 60 * 60 * 10000, { long: true })).toBe("-10 months");
|
||||
});
|
||||
|
||||
it("should support years", () => {
|
||||
expect(ms(365.25 * 24 * 60 * 60 * 1000 + 1, { long: true })).toBe("1 year");
|
||||
expect(ms(365.25 * 24 * 60 * 60 * 1200 + 1, { long: true })).toBe("1 year");
|
||||
expect(ms(365.25 * 24 * 60 * 60 * 10000 + 1, { long: true })).toBe("10 years");
|
||||
|
||||
expect(ms(-1 * 365.25 * 24 * 60 * 60 * 1000 - 1, { long: true })).toBe("-1 year");
|
||||
expect(ms(-1 * 365.25 * 24 * 60 * 60 * 1200 - 1, { long: true })).toBe("-1 year");
|
||||
expect(ms(-1 * 365.25 * 24 * 60 * 60 * 10000 - 1, { long: true })).toBe("-10 years");
|
||||
});
|
||||
|
||||
it("should round", () => {
|
||||
expect(ms(234234234, { long: true })).toBe("3 days");
|
||||
|
||||
expect(ms(-234234234, { long: true })).toBe("-3 days");
|
||||
});
|
||||
});
|
||||
|
||||
// numbers
|
||||
|
||||
describe("ms(number)", () => {
|
||||
it("should not throw an error", () => {
|
||||
expect(() => {
|
||||
ms(500);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should support milliseconds", () => {
|
||||
expect(ms(500)).toBe("500ms");
|
||||
|
||||
expect(ms(-500)).toBe("-500ms");
|
||||
});
|
||||
|
||||
it("should support seconds", () => {
|
||||
expect(ms(1000)).toBe("1s");
|
||||
expect(ms(10000)).toBe("10s");
|
||||
|
||||
expect(ms(-1000)).toBe("-1s");
|
||||
expect(ms(-10000)).toBe("-10s");
|
||||
});
|
||||
|
||||
it("should support minutes", () => {
|
||||
expect(ms(60 * 1000)).toBe("1m");
|
||||
expect(ms(60 * 10000)).toBe("10m");
|
||||
|
||||
expect(ms(-1 * 60 * 1000)).toBe("-1m");
|
||||
expect(ms(-1 * 60 * 10000)).toBe("-10m");
|
||||
});
|
||||
|
||||
it("should support hours", () => {
|
||||
expect(ms(60 * 60 * 1000)).toBe("1h");
|
||||
expect(ms(60 * 60 * 10000)).toBe("10h");
|
||||
|
||||
expect(ms(-1 * 60 * 60 * 1000)).toBe("-1h");
|
||||
expect(ms(-1 * 60 * 60 * 10000)).toBe("-10h");
|
||||
});
|
||||
|
||||
it("should support days", () => {
|
||||
expect(ms(24 * 60 * 60 * 1000)).toBe("1d");
|
||||
expect(ms(24 * 60 * 60 * 6000)).toBe("6d");
|
||||
|
||||
expect(ms(-1 * 24 * 60 * 60 * 1000)).toBe("-1d");
|
||||
expect(ms(-1 * 24 * 60 * 60 * 6000)).toBe("-6d");
|
||||
});
|
||||
|
||||
it("should support weeks", () => {
|
||||
expect(ms(1 * 7 * 24 * 60 * 60 * 1000)).toBe("1w");
|
||||
expect(ms(2 * 7 * 24 * 60 * 60 * 1000)).toBe("2w");
|
||||
|
||||
expect(ms(-1 * 1 * 7 * 24 * 60 * 60 * 1000)).toBe("-1w");
|
||||
expect(ms(-1 * 2 * 7 * 24 * 60 * 60 * 1000)).toBe("-2w");
|
||||
});
|
||||
|
||||
it("should support months", () => {
|
||||
expect(ms(30.4375 * 24 * 60 * 60 * 1000)).toBe("1mo");
|
||||
expect(ms(30.4375 * 24 * 60 * 60 * 1200)).toBe("1mo");
|
||||
expect(ms(30.4375 * 24 * 60 * 60 * 10000)).toBe("10mo");
|
||||
|
||||
expect(ms(-1 * 30.4375 * 24 * 60 * 60 * 1000)).toBe("-1mo");
|
||||
expect(ms(-1 * 30.4375 * 24 * 60 * 60 * 1200)).toBe("-1mo");
|
||||
expect(ms(-1 * 30.4375 * 24 * 60 * 60 * 10000)).toBe("-10mo");
|
||||
});
|
||||
|
||||
it("should support years", () => {
|
||||
expect(ms(365.25 * 24 * 60 * 60 * 1000 + 1)).toBe("1y");
|
||||
expect(ms(365.25 * 24 * 60 * 60 * 1200 + 1)).toBe("1y");
|
||||
expect(ms(365.25 * 24 * 60 * 60 * 10000 + 1)).toBe("10y");
|
||||
|
||||
expect(ms(-1 * 365.25 * 24 * 60 * 60 * 1000 - 1)).toBe("-1y");
|
||||
expect(ms(-1 * 365.25 * 24 * 60 * 60 * 1200 - 1)).toBe("-1y");
|
||||
expect(ms(-1 * 365.25 * 24 * 60 * 60 * 10000 - 1)).toBe("-10y");
|
||||
});
|
||||
|
||||
it("should round", () => {
|
||||
expect(ms(234234234)).toBe("3d");
|
||||
|
||||
expect(ms(-234234234)).toBe("-3d");
|
||||
});
|
||||
});
|
||||
|
||||
// invalid inputs
|
||||
|
||||
describe("ms(invalid inputs)", () => {
|
||||
it('should throw an error, when ms("")', () => {
|
||||
expect(() => {
|
||||
// @ts-expect-error - We expect this to throw.
|
||||
ms("");
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should throw an error, when ms(undefined)", () => {
|
||||
expect(() => {
|
||||
// @ts-expect-error - We expect this to throw.
|
||||
ms(undefined);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should throw an error, when ms(null)", () => {
|
||||
expect(() => {
|
||||
// @ts-expect-error - We expect this to throw.
|
||||
ms(null);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should throw an error, when ms([])", () => {
|
||||
expect(() => {
|
||||
// @ts-expect-error - We expect this to throw.
|
||||
ms([]);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should throw an error, when ms({})", () => {
|
||||
expect(() => {
|
||||
// @ts-expect-error - We expect this to throw.
|
||||
ms({});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should throw an error, when ms(NaN)", () => {
|
||||
expect(() => {
|
||||
ms(NaN);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should throw an error, when ms(Infinity)", () => {
|
||||
expect(() => {
|
||||
ms(Infinity);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should throw an error, when ms(-Infinity)", () => {
|
||||
expect(() => {
|
||||
ms(-Infinity);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user