aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2023-05-23 00:40:12 -0700
committerGravatar GitHub <noreply@github.com> 2023-05-23 00:40:12 -0700
commit5b38c55c3db018a94505f61cd785f0dd40f442ac (patch)
treeaf522e38ffa9b6c400c500c76de1fdca4ab931db
parent83e7b9e198b25c7af7905c5dcabe1e325c5a38fb (diff)
downloadbun-5b38c55c3db018a94505f61cd785f0dd40f442ac.tar.gz
bun-5b38c55c3db018a94505f61cd785f0dd40f442ac.tar.zst
bun-5b38c55c3db018a94505f61cd785f0dd40f442ac.zip
Support setting a timezone with `process.env.TZ` and `Bun.env.TZ` (#3018)
* Support setting a timezone via `process.env.TZ` * Implement `setTimeZone` in `bun:jsc` module * [breaking] `bun:test` now defaults to `Etc/UTC` timezone --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r--.vscode/launch.json3
-rw-r--r--packages/bun-types/bun.d.ts15
-rw-r--r--packages/bun-types/jsc.d.ts12
-rw-r--r--src/bun.js/bindings/BunJSCModule.cpp33
-rw-r--r--src/bun.js/bindings/JSEnvironmentVariableMap.cpp72
-rw-r--r--src/bun.js/bindings/ZigGlobalObject.cpp12
-rw-r--r--src/bun.js/bindings/bindings.zig5
-rw-r--r--src/bun.js/bun-jsc.exports.js2
-rw-r--r--src/bun_js.zig7
-rw-r--r--src/cli/test_command.zig13
-rw-r--r--test/js/bun/jsc/bun-jsc.test.ts30
-rw-r--r--test/js/node/process/process.test.js22
12 files changed, 225 insertions, 1 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e196eda42..08ecd7f8f 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -34,7 +34,8 @@
"cwd": "${workspaceFolder}/test",
"env": {
"FORCE_COLOR": "1",
- "BUN_GARBAGE_COLLECTOR_LEVEL": "2"
+ "BUN_GARBAGE_COLLECTOR_LEVEL": "2",
+ "BUN_DEBUG_QUIET_LOGS": "1"
},
"console": "internalConsole"
},
diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts
index 698bb8b2b..acd80124f 100644
--- a/packages/bun-types/bun.d.ts
+++ b/packages/bun-types/bun.d.ts
@@ -5,6 +5,21 @@ interface VoidFunction {
declare namespace Bun {
interface Env extends Dict<string> {
NODE_ENV: string;
+
+ /**
+ * The timezone used by Intl, Date, etc.
+ *
+ * To change the timezone, set `Bun.env.TZ` or `process.env.TZ` to the time zone you want to use.
+ *
+ * You can view the current timezone with `Intl.DateTimeFormat().resolvedOptions().timeZone`
+ *
+ * @example
+ * ```js
+ * Bun.env.TZ = "America/Los_Angeles";
+ * console.log(Intl.DateTimeFormat().resolvedOptions().timeZone); // "America/Los_Angeles"
+ * ```
+ */
+ TZ?: string;
}
}
diff --git a/packages/bun-types/jsc.d.ts b/packages/bun-types/jsc.d.ts
index 06e6d0fd3..08e8ba680 100644
--- a/packages/bun-types/jsc.d.ts
+++ b/packages/bun-types/jsc.d.ts
@@ -40,6 +40,18 @@ declare module "bun:jsc" {
export function drainMicrotasks(): void;
/**
+ * Set the timezone used by Intl, Date, etc.
+ *
+ * @param timeZone A string representing the time zone to use, such as "America/Los_Angeles"
+ *
+ * @returns The normalized time zone string
+ *
+ * You can also set process.env.TZ to the time zone you want to use.
+ * You can also view the current timezone with `Intl.DateTimeFormat().resolvedOptions().timeZone`
+ */
+ export function setTimeZone(timeZone: string): string;
+
+ /**
* Run JavaScriptCore's sampling profiler for a particular function
*
* This is pretty low-level.
diff --git a/src/bun.js/bindings/BunJSCModule.cpp b/src/bun.js/bindings/BunJSCModule.cpp
index 63721a878..5809a9813 100644
--- a/src/bun.js/bindings/BunJSCModule.cpp
+++ b/src/bun.js/bindings/BunJSCModule.cpp
@@ -420,6 +420,38 @@ JSC_DEFINE_HOST_FUNCTION(functionDrainMicrotasks, (JSGlobalObject * globalObject
return JSValue::encode(jsUndefined());
}
+JSC_DEFINE_HOST_FUNCTION(functionSetTimeZone, (JSGlobalObject * globalObject, CallFrame* callFrame))
+{
+ VM& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+
+ if (callFrame->argumentCount() < 1) {
+ throwTypeError(globalObject, scope, "setTimeZone requires a timezone string"_s);
+ return encodedJSValue();
+ }
+
+ if (!callFrame->argument(0).isString()) {
+ throwTypeError(globalObject, scope, "setTimeZone requires a timezone string"_s);
+ return encodedJSValue();
+ }
+
+ String timeZoneName = callFrame->argument(0).toWTFString(globalObject);
+ RETURN_IF_EXCEPTION(scope, encodedJSValue());
+
+ double time = callFrame->argument(1).toNumber(globalObject);
+ RETURN_IF_EXCEPTION(scope, encodedJSValue());
+
+ if (!WTF::setTimeZoneOverride(timeZoneName)) {
+ throwTypeError(globalObject, scope, makeString("Invalid timezone: \""_s, timeZoneName, "\""_s));
+ return encodedJSValue();
+ }
+ vm.dateCache.resetIfNecessarySlow();
+ WTF::Vector<UChar, 32> buffer;
+ WTF::getTimeZoneOverride(buffer);
+ WTF::String timeZoneString(buffer.data(), buffer.size());
+ return JSValue::encode(jsString(vm, timeZoneString));
+}
+
JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
JSC::VM& vm = globalObject->vm();
@@ -528,6 +560,7 @@ JSC::JSObject* createJSCModule(JSC::JSGlobalObject* globalObject)
object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "getProtectedObjects"_s), 1, functionGetProtectedObjects, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0);
object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "generateHeapSnapshotForDebugging"_s), 0, functionGenerateHeapSnapshotForDebugging, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0);
object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "profile"_s), 0, functionRunProfiler, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0);
+ object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "setTimeZone"_s), 0, functionSetTimeZone, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0);
}
return object;
diff --git a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp
index b90d9f44c..5c0357066 100644
--- a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp
+++ b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp
@@ -50,6 +50,64 @@ JSC_DEFINE_CUSTOM_SETTER(jsSetterEnvironmentVariable, (JSGlobalObject * globalOb
return true;
}
+JSC_DEFINE_CUSTOM_GETTER(jsTimeZoneEnvironmentVariableGetter, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName propertyName))
+{
+ VM& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+
+ auto* thisObject = jsDynamicCast<JSObject*>(JSValue::decode(thisValue));
+ if (UNLIKELY(!thisObject))
+ return JSValue::encode(jsUndefined());
+
+ auto* clientData = WebCore::clientData(vm);
+
+ ZigString name = toZigString(propertyName.publicName());
+ ZigString value = { nullptr, 0 };
+
+ if (auto hasExistingValue = thisObject->getIfPropertyExists(globalObject, clientData->builtinNames().dataPrivateName())) {
+ return JSValue::encode(hasExistingValue);
+ }
+
+ if (!Bun__getEnvValue(globalObject, &name, &value) || value.len == 0) {
+ return JSValue::encode(jsUndefined());
+ }
+
+ JSValue out = jsString(vm, Zig::toStringCopy(value));
+ thisObject->putDirect(vm, clientData->builtinNames().dataPrivateName(), out, 0);
+
+ return JSValue::encode(out);
+}
+
+// In Node.js, the "TZ" environment variable is special.
+// Setting it automatically updates the timezone.
+// We also expose an explicit setTimeZone function in bun:jsc
+JSC_DEFINE_CUSTOM_SETTER(jsTimeZoneEnvironmentVariableSetter, (JSGlobalObject * globalObject, EncodedJSValue thisValue, EncodedJSValue value, PropertyName propertyName))
+{
+ VM& vm = globalObject->vm();
+ JSC::JSObject* object = JSValue::decode(thisValue).getObject();
+ if (!object)
+ return false;
+
+ JSValue decodedValue = JSValue::decode(value);
+ if (decodedValue.isString()) {
+ auto timeZoneName = decodedValue.toWTFString(globalObject);
+ if (timeZoneName.length() < 32) {
+ if (WTF::setTimeZoneOverride(timeZoneName)) {
+ vm.dateCache.resetIfNecessarySlow();
+ }
+ }
+ }
+
+ auto* clientData = WebCore::clientData(vm);
+ auto* builtinNames = &clientData->builtinNames();
+ auto privateName = builtinNames->dataPrivateName();
+ object->putDirect(vm, privateName, JSValue::decode(value), 0);
+
+ // Recreate this because the property visibility needs to be set correctly
+ object->putDirectCustomAccessor(vm, propertyName, JSC::CustomGetterSetter::create(vm, jsTimeZoneEnvironmentVariableGetter, jsTimeZoneEnvironmentVariableSetter), JSC::PropertyAttribute::CustomAccessor | 0);
+ return true;
+}
+
JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject)
{
VM& vm = globalObject->vm();
@@ -65,11 +123,25 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject)
object = constructEmptyObject(globalObject, globalObject->objectPrototype());
}
+ static NeverDestroyed<String> TZ = MAKE_STATIC_STRING_IMPL("TZ");
+ bool hasTZ = false;
for (size_t i = 0; i < count; i++) {
auto name = Zig::toStringCopy(names[i]);
+ if (name == TZ) {
+ hasTZ = true;
+ continue;
+ }
object->putDirectCustomAccessor(vm, Identifier::fromString(vm, name), JSC::CustomGetterSetter::create(vm, jsGetterEnvironmentVariable, jsSetterEnvironmentVariable), JSC::PropertyAttribute::CustomAccessor | 0);
}
+ unsigned int TZAttrs = JSC::PropertyAttribute::CustomAccessor | 0;
+ if (!hasTZ) {
+ TZAttrs |= JSC::PropertyAttribute::DontEnum;
+ }
+ object->putDirectCustomAccessor(
+ vm,
+ Identifier::fromString(vm, TZ), JSC::CustomGetterSetter::create(vm, jsTimeZoneEnvironmentVariableGetter, jsTimeZoneEnvironmentVariableSetter), TZAttrs);
+
return object;
}
} \ No newline at end of file
diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp
index 663c2a491..b3da8a98f 100644
--- a/src/bun.js/bindings/ZigGlobalObject.cpp
+++ b/src/bun.js/bindings/ZigGlobalObject.cpp
@@ -3768,6 +3768,18 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor)
visitor.addOpaqueRoot(context);
}
+extern "C" bool JSGlobalObject__setTimeZone(JSC::JSGlobalObject* globalObject, const ZigString* timeZone)
+{
+ auto& vm = globalObject->vm();
+
+ if (WTF::setTimeZoneOverride(Zig::toString(*timeZone))) {
+ vm.dateCache.resetIfNecessarySlow();
+ return true;
+ }
+
+ return false;
+}
+
extern "C" void JSGlobalObject__throwTerminationException(JSC::JSGlobalObject* globalObject)
{
globalObject->vm().setHasTerminationRequest();
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index 373bca8ec..a03737119 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -2354,6 +2354,11 @@ pub const JSGlobalObject = extern struct {
extern fn JSGlobalObject__throwTerminationException(this: *JSGlobalObject) void;
pub const throwTerminationException = JSGlobalObject__throwTerminationException;
pub const clearTerminationException = JSGlobalObject__clearTerminationException;
+ extern fn JSGlobalObject__setTimeZone(this: *JSGlobalObject, timeZone: *const ZigString) bool;
+
+ pub fn setTimeZone(this: *JSGlobalObject, timeZone: *const ZigString) bool {
+ return JSGlobalObject__setTimeZone(this, timeZone);
+ }
pub fn throwInvalidArguments(
this: *JSGlobalObject,
diff --git a/src/bun.js/bun-jsc.exports.js b/src/bun.js/bun-jsc.exports.js
index 765f9aeb5..d49e41851 100644
--- a/src/bun.js/bun-jsc.exports.js
+++ b/src/bun.js/bun-jsc.exports.js
@@ -31,3 +31,5 @@ export const getProtectedObjects = jsc.getProtectedObjects;
export const generateHeapSnapshotForDebugging = jsc.generateHeapSnapshotForDebugging;
export const profile = jsc.profile;
export default jsc;
+export const setTimeZone = jsc.setTimeZone;
+export const setTimezone = setTimeZone;
diff --git a/src/bun_js.zig b/src/bun_js.zig
index 5a4eb4f8a..00cb51d20 100644
--- a/src/bun_js.zig
+++ b/src/bun_js.zig
@@ -204,6 +204,13 @@ pub const Run = struct {
vm.is_main_thread = true;
JSC.VirtualMachine.is_main_thread_vm = true;
+ // Allow setting a custom timezone
+ if (vm.bundler.env.get("TZ")) |tz| {
+ if (tz.len > 0) {
+ _ = vm.global.setTimeZone(&JSC.ZigString.init(tz));
+ }
+ }
+
var callback = OpaqueWrap(Run, Run.start);
vm.global.vm().holdAPILock(&run, callback);
}
diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig
index 996289ac4..8330a786b 100644
--- a/src/cli/test_command.zig
+++ b/src/cli/test_command.zig
@@ -382,6 +382,7 @@ const Scanner = struct {
pub const TestCommand = struct {
pub const name = "test";
pub const old_name = "wiptest";
+
pub fn exec(ctx: Command.Context) !void {
if (comptime is_bindgen) unreachable;
// print the version so you know its doing stuff if it takes a sec
@@ -460,6 +461,18 @@ pub const TestCommand = struct {
vm.is_main_thread = true;
JSC.VirtualMachine.is_main_thread_vm = true;
+ // For tests, we default to UTC time zone
+ // unless the user inputs TZ="", in which case we use local time zone
+ var TZ_NAME: string =
+ // We use the string "Etc/UTC" instead of "UTC" so there is no normalization difference.
+ "Etc/UTC";
+ if (vm.bundler.env.get("TZ")) |tz| {
+ TZ_NAME = tz;
+ }
+ if (TZ_NAME.len > 0) {
+ _ = vm.global.setTimeZone(&JSC.ZigString.init(TZ_NAME));
+ }
+
var scanner = Scanner{
.dirs_to_scan = Scanner.Fifo.init(ctx.allocator),
.options = &vm.bundler.options,
diff --git a/test/js/bun/jsc/bun-jsc.test.ts b/test/js/bun/jsc/bun-jsc.test.ts
index aa93ce90a..f16a6a781 100644
--- a/test/js/bun/jsc/bun-jsc.test.ts
+++ b/test/js/bun/jsc/bun-jsc.test.ts
@@ -22,6 +22,7 @@ import {
reoptimizationRetryCount,
drainMicrotasks,
startRemoteDebugger,
+ setTimeZone,
} from "bun:jsc";
describe("bun:jsc", () => {
@@ -110,4 +111,33 @@ describe("bun:jsc", () => {
it("getProtectedObjects", () => {
expect(getProtectedObjects().length).toBeGreaterThan(0);
});
+
+ it("setTimeZone", () => {
+ var origTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ const realOrigTimezone = origTimezone;
+ if (origTimezone === "America/Anchorage") {
+ origTimezone = "America/New_York";
+ }
+ const origDate = new Date();
+ origDate.setSeconds(0);
+ origDate.setMilliseconds(0);
+ origDate.setMinutes(0);
+ const origDateString = origDate.toString();
+ expect(origTimezone).toBeDefined();
+ expect(origTimezone).not.toBe("America/Anchorage");
+ expect(setTimeZone("America/Anchorage")).toBe("America/Anchorage");
+ expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe("America/Anchorage");
+ if (realOrigTimezone === origTimezone) {
+ const newDate = new Date();
+ newDate.setSeconds(0);
+ newDate.setMilliseconds(0);
+ newDate.setMinutes(0);
+ const newDateString = newDate.toString();
+ expect(newDateString).not.toBe(origDateString);
+ }
+
+ expect(setTimeZone(realOrigTimezone)).toBe(realOrigTimezone);
+
+ expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe(origTimezone);
+ });
});
diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js
index 414392a79..c9f92362c 100644
--- a/test/js/node/process/process.test.js
+++ b/test/js/node/process/process.test.js
@@ -98,6 +98,28 @@ it("process.env is spreadable and editable", () => {
expect(eval(`globalThis.process.env.USER = "${orig}"`)).toBe(orig);
});
+it("process.env.TZ", () => {
+ var origTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ // the default timezone is Etc/UTC
+ if (!"TZ" in process.env) {
+ expect(origTimezone).toBe("Etc/UTC");
+ }
+
+ const realOrigTimezone = origTimezone;
+ if (origTimezone === "America/Anchorage") {
+ origTimezone = "America/New_York";
+ }
+
+ const target = "America/Anchorage";
+ const tzKey = String("TZ" + " ").substring(0, 2);
+ process.env[tzKey] = target;
+ expect(process.env[tzKey]).toBe(target);
+ expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe(target);
+ process.env[tzKey] = origTimezone;
+ expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe(realOrigTimezone);
+});
+
it("process.version starts with v", () => {
expect(process.version.startsWith("v")).toBeTruthy();
});