@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
614 lines • 24.8 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { TZ_ABBREVS } from "./tz_abbrevs.js";
export const TZ_VAR = 'TZ';
const HOUR = 3600;
const DAY_MS = 86_400_000;
// Character classes of a POSIX TZ string, ASCII only as in the C locale:
// `ÄBC5` has no name GNU accepts.
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const DIGITS = '0123456789';
const QUOTABLE = `${LETTERS}${DIGITS}+-`;
const BLANKS = ' \t\n\v\f\r';
const SIGNS = '+-';
// A numeric field holds 16 bits: `65536` reads as 0, `-5` as 65531, and
// anything past 2**64 - 1 as 65535.
const FIELD_SPAN = 0x10000;
const FIELD_CEILING = '18446744073709551615';
// GNU prints offsets up to ±24:59:59; the Python twin's tzinfo must stay
// inside a day, so both hosts stop one second short of it.
const OFFSET_LIMIT = 24 * HOUR - 1;
/**
* A Date from wall-clock fields read as UTC. `Date.UTC` reads a year below
* 100 as 1900 plus that year; the setters do not, so a year GNU and Python
* accept as itself (`0042-01-01`) lands where it belongs. Overflowing
* fields carry, so day 32 is the next month's first.
*/
export function utcFromWall(p) {
const d = new Date(0);
d.setUTCFullYear(p.year, p.month, p.day);
d.setUTCHours(p.hour, p.minute, p.second, p.ms);
return d;
}
function utcWall(dt) {
return {
year: dt.getUTCFullYear(),
month: dt.getUTCMonth(),
day: dt.getUTCDate(),
hour: dt.getUTCHours(),
minute: dt.getUTCMinutes(),
second: dt.getUTCSeconds(),
ms: dt.getUTCMilliseconds(),
weekday: dt.getUTCDay(),
};
}
class UtcZone {
parts(dt) {
return { ...utcWall(dt), offsetSec: 0, abbrev: 'UTC' };
}
fromWall(p) {
return utcFromWall(p);
}
}
// Resolve the host zone at the instant being formatted, just as explicit TZ does.
class LocalZone {
parts(dt) {
return {
year: dt.getFullYear(),
month: dt.getMonth(),
day: dt.getDate(),
hour: dt.getHours(),
minute: dt.getMinutes(),
second: dt.getSeconds(),
ms: dt.getMilliseconds(),
weekday: dt.getDay(),
offsetSec: -dt.getTimezoneOffset() * 60,
abbrev: tzAbbreviation(new Intl.DateTimeFormat().resolvedOptions().timeZone, -dt.getTimezoneOffset() * 60, Math.floor(dt.getTime() / 1000)),
};
}
fromWall(p) {
const d = new Date(0);
d.setFullYear(p.year, p.month, p.day);
d.setHours(p.hour, p.minute, p.second, p.ms);
return d;
}
}
/** One offset under one abbreviation: `UTC0`, `JST-9`, `<+0530>-5:30`. */
class FixedZone {
offsetSec;
abbrev;
constructor(offsetSec, abbrev) {
this.offsetSec = offsetSec;
this.abbrev = abbrev;
}
parts(dt) {
const shown = utcWall(new Date(dt.getTime() + this.offsetSec * 1000));
return { ...shown, offsetSec: this.offsetSec, abbrev: this.abbrev };
}
fromWall(p) {
return new Date(utcFromWall(p).getTime() - this.offsetSec * 1000);
}
}
/**
* The abbreviation tzdata gives an offset it has no letters for: `+08`,
* `-03`, `+0530`, with minutes only when they are not zero.
*/
export function numericAbbreviation(offsetSec) {
const sign = offsetSec < 0 ? '-' : '+';
const total = Math.abs(offsetSec);
const hours = String(Math.floor(total / 3600)).padStart(2, '0');
const minutes = Math.floor((total % 3600) / 60);
return minutes === 0 ? `${sign}${hours}` : `${sign}${hours}${String(minutes).padStart(2, '0')}`;
}
/**
* The abbreviation tzdata gives `name` at `offsetSec` as of `atSec` (epoch
* seconds), which is what GNU date prints for `%Z` and what Python's
* zoneinfo renders. Intl only offers `GMT+8`, so the names ship in
* TZ_ABBREVS (generated from zoneinfo by scripts/gen_tz_abbrevs.py), one
* row per name an offset has carried with the moment it took effect: the
* latest row for the offset that had taken effect by `atSec` wins, the
* earliest stands in before the table's 1970 start, and an offset with no
* row is one tzdata spells out (`+08`).
*/
export function tzAbbreviation(name, offsetSec, atSec) {
let found;
for (const [offset, abbrev, since] of TZ_ABBREVS[name] ?? []) {
if (offset !== offsetSec)
continue;
if (found === undefined || since <= atSec)
found = abbrev;
}
return found ?? numericAbbreviation(offsetSec);
}
/**
* A tzdata zone, read through Intl for its offsets and through
* tzAbbreviation for its `%Z`, so `Asia/Hong_Kong` renders `HKT` here as
* it does under GNU date and in the Python twin.
*/
class IntlZone {
name;
format;
constructor(name) {
this.name = name;
this.format = new Intl.DateTimeFormat('en-US', {
timeZone: name,
hourCycle: 'h23',
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
});
}
parts(dt) {
const fields = {};
for (const part of this.format.formatToParts(dt))
fields[part.type] = part.value;
const wall = {
year: Number(fields.year),
month: Number(fields.month) - 1,
day: Number(fields.day),
hour: Number(fields.hour) % 24,
minute: Number(fields.minute),
second: Number(fields.second),
ms: dt.getUTCMilliseconds(),
};
const shown = utcFromWall(wall);
const offsetSec = Math.round((shown.getTime() - dt.getTime()) / 1000);
return {
...wall,
weekday: shown.getUTCDay(),
offsetSec,
abbrev: tzAbbreviation(this.name, offsetSec, Math.floor(dt.getTime() / 1000)),
};
}
fromWall(p, prefer) {
const wall = utcFromWall(p).getTime();
// The offsets a day before and a day after the wall clock read as UTC
// bracket any change near it; reading under each gives the candidate
// instants, both of which show `p` in a repeated hour and neither of
// which does in a skipped one.
const before = this.parts(new Date(wall - DAY_MS)).offsetSec;
const after = this.parts(new Date(wall + DAY_MS)).offsetSec;
const candidates = before === after ? [wall - before * 1000] : [wall - before * 1000, wall - after * 1000];
const shown = candidates.filter((t) => utcFromWall(this.parts(new Date(t))).getTime() === wall);
if (shown.length === 0)
return new Date(wall - before * 1000);
const kept = shown.find((t) => this.parts(new Date(t)).offsetSec === prefer);
return new Date(kept ?? Math.max(...shown));
}
}
// What a DST zone falls back to when its string names no rule, or only a
// start: DST from 02:00 on the second Sunday of March to 02:00 on the first
// Sunday of November.
const FALLBACK_START = {
kind: 'M',
month: 3,
week: 2,
weekday: 0,
day: 0,
seconds: 2 * HOUR,
};
const FALLBACK_END = { ...FALLBACK_START, month: 11, week: 1 };
// A rule GNU never read sits at midnight on January 1; a `J` or `Mm` rule
// whose day or month it refused at midnight on the day before.
const UNREAD_RULE = { kind: 'D', month: 0, week: 0, weekday: 0, day: 0, seconds: 0 };
const DAY_BEFORE_YEAR = { ...UNREAD_RULE, kind: 'J' };
// Days in `month` (1 for January) of `year`: day 0 of the next month.
export function daysInMonth(year, month) {
return utcFromWall({ year, month, day: 0, hour: 0, minute: 0, second: 0, ms: 0 }).getUTCDate();
}
// Midnight of a calendar day, as a Date read as UTC; `day` may overflow.
function midnightOf(year, month, day) {
return utcFromWall({ year, month, day, hour: 0, minute: 0, second: 0, ms: 0 });
}
/**
* The day an `Mm.w.d` rule names. POSIX: week 1 holds the first `weekday`
* of the month and week 5 the last. Measured on GNU date 9.7 beyond that:
* week 0 reads as 1 and a week past the last occurrence as the last one; a
* weekday past 6 counts that many days from the Sunday that opens the
* month's first week (M9.1.7 is 2024-09-08, as September 2024 starts on a
* Sunday), and a count that runs past the month stays there (M3.5.100 is
* 2024-06-04). `month` is 1 for January.
*/
function weekdayInMonth(year, month, week, weekday) {
const first = midnightOf(year, month - 1, 1);
const opensOn = first.getUTCDay();
const offset = weekday <= 6 ? (weekday - opensOn + 7) % 7 : weekday - opensOn;
const last = daysInMonth(year, month) - 1;
// How many of that day the month holds from the first one on: one even
// when the first already runs past the month.
const held = 1 + Math.max(0, Math.floor((last - offset) / 7));
const nth = Math.min(Math.max(week, 1), held);
return new Date(first.getTime() + (offset + 7 * (nth - 1)) * DAY_MS);
}
/**
* The day a `Jn` rule names: the `day`-th of a year counted as if February
* always had 28 days, so `J60` is March 1 in every year; day 0 is December
* 31 of the year before.
*/
function julianDate(year, day) {
if (day === 0)
return midnightOf(year, 0, 0);
// 2001 has no February 29, so its calendar is the one J counts in.
const common = midnightOf(2001, 0, day);
return midnightOf(year, common.getUTCMonth(), common.getUTCDate());
}
/** The transition's wall-clock moment in `year`, as ms of that wall clock read as UTC. */
export function transitionAt(rule, year) {
let day;
if (rule.kind === 'M')
day = weekdayInMonth(year, rule.month, rule.week, rule.weekday);
else if (rule.kind === 'J')
day = julianDate(year, rule.day);
else
day = midnightOf(year, 0, 1 + rule.day);
return day.getTime() + rule.seconds * 1000;
}
/**
* A zone read from a POSIX TZ string with a DST half
* (`CET-1CEST,M3.5.0,M10.5.0/3`). The two offsets and the two rules decide
* everything, as measured on GNU date 9.7: both of a year's transitions are
* laid out in the year the UTC clock reads, the start in standard wall time
* and the end in DST wall time, and an instant shows the state the latest
* of them up to it set, or, before either, the state the year ends in, so a
* southern window wraps the new year and two transitions at one instant
* leave DST off. The DST half may be nameless with a zero offset, which is
* what GNU date shows when it cannot read that name.
*/
class PosixZone {
std;
stdOffsetSec;
dst;
dstOffsetSec;
start;
end;
constructor(std, stdOffsetSec, dst, dstOffsetSec, start, end) {
this.std = std;
this.stdOffsetSec = stdOffsetSec;
this.dst = dst;
this.dstOffsetSec = dstOffsetSec;
this.start = start;
this.end = end;
}
inDst(utcMs) {
const year = new Date(utcMs).getUTCFullYear();
const start = transitionAt(this.start, year) - this.stdOffsetSec * 1000;
const end = transitionAt(this.end, year) - this.dstOffsetSec * 1000;
// The year's two changes in order, an end after a start at the same
// instant: the latest one passed sets the state, and before either the
// year's last change still holds from the year before.
let early = [start, true];
let late = [end, false];
if (end < start)
[early, late] = [late, early];
if (utcMs >= late[0])
return late[1];
return utcMs >= early[0] ? early[1] : late[1];
}
/**
* When daylight saving time starts and ends in a UTC year, as epoch seconds:
* the instants the two rules lay out in the year the UTC clock reads, the
* end first when the window wraps the year and the two equal when it is
* empty. Mirrors Python's PosixZone.transitions.
*/
transitions(year) {
const start = transitionAt(this.start, year) - this.stdOffsetSec * 1000;
const end = transitionAt(this.end, year) - this.dstOffsetSec * 1000;
return [Math.floor(start / 1000), Math.floor(end / 1000)];
}
parts(dt) {
const inDst = this.inDst(dt.getTime());
const offsetSec = inDst ? this.dstOffsetSec : this.stdOffsetSec;
const shown = utcWall(new Date(dt.getTime() + offsetSec * 1000));
return { ...shown, offsetSec, abbrev: inDst ? this.dst : this.std };
}
fromWall(p, prefer) {
const wall = utcFromWall(p).getTime();
const asStd = wall - this.stdOffsetSec * 1000;
const asDst = wall - this.dstOffsetSec * 1000;
const stdShows = !this.inDst(asStd);
const dstShows = this.inDst(asDst);
// A repeated hour: the reading on the base's side of the change, else
// standard time, the later of the two, which is the one GNU date 9.7
// picks. A skipped hour reads under standard time, the offset in force
// before the change.
if (stdShows && dstShows)
return new Date(prefer === this.dstOffsetSec ? asDst : asStd);
if (dstShows)
return new Date(asDst);
return new Date(asStd);
}
}
/**
* When a POSIX TZ zone's daylight saving time starts and ends in a UTC year
* (see PosixZone.transitions), null for any other zone, whose transitions come
* from tzdata. Mirrors Python's posix_transitions.
*/
export function posixTransitions(zone, year) {
return zone instanceof PosixZone ? zone.transitions(year) : null;
}
export const UTC_ZONE = new UtcZone();
export const LOCAL_ZONE = new LocalZone();
/**
* The zone a command environment's TZ names, or null when TZ is unset (or
* there is no environment), which means the host's local zone.
*/
export function zoneFromEnv(env) {
const spec = env?.[TZ_VAR];
if (spec === undefined)
return null;
return resolveTz(spec);
}
/**
* The zone a TZ value names, read as GNU date 9.7 reads it: a leading
* colon is dropped; an empty value is UTC; a name tzdata knows
* (`Asia/Hong_Kong`, `UTC`, `EST5EDT`) is that zone; anything else is a
* POSIX TZ string (`UTC0`, `JST-9`, `<+0530>-5:30`,
* `CET-1CEST,M3.5.0,M10.5.0/3`), where a bare name with no offset is UTC
* under that name, which is how GNU date renders `TZ=Bogus/Zone`
* (`+0000 Bogus`), and a name shorter than three letters is refused,
* leaving `%Z` empty.
*/
export function resolveTz(spec) {
const name = spec.startsWith(':') ? spec.slice(1) : spec;
if (name === '')
return UTC_ZONE;
try {
return new IntlZone(name);
}
catch (err) {
if (!(err instanceof RangeError))
throw err;
return posixZone(name);
}
}
// Where the run of `chars` starting at `pos` ends.
function spanOf(spec, pos, chars) {
while (pos < spec.length && chars.includes(spec.charAt(pos)))
pos++;
return pos;
}
/**
* A numeric field of an offset, a rule time or an `M` rule: GNU skips
* blanks before the digits and takes one sign, then keeps the value in 16
* bits, so `UTC65559` is -23:00, `UTC--65535` (the offset's sign, then
* -65535) is +01:00, and `UTC99999999999999999999` reads its hours as
* 65535. Returns the value and the position after its digits, or null when
* no digit follows.
*/
function numberField(spec, pos) {
let start = spanOf(spec, pos, BLANKS);
const negative = spec.startsWith('-', start);
if (start < spec.length && SIGNS.includes(spec.charAt(start)))
start++;
const stop = spanOf(spec, start, DIGITS);
if (stop === start)
return null;
const digits = spec.slice(start, stop).replace(/^0+/, '');
if (digits.length > FIELD_CEILING.length ||
(digits.length === FIELD_CEILING.length && digits > FIELD_CEILING)) {
return [FIELD_SPAN - 1, stop];
}
let value = 0;
for (const digit of digits)
value = (value * 10 + Number(digit)) % FIELD_SPAN;
return [((negative ? -value : value) + FIELD_SPAN) % FIELD_SPAN, stop];
}
/**
* `hh[:mm[:ss]]`, each a numeric field. A colon with no field after it is
* left unread, so `UTC5:` is -05:00 followed by `:`. Returns hours, minutes
* and seconds (0 when absent) and the position after the last field read,
* or null when there are no hours.
*/
function clockFields(spec, pos) {
const hours = numberField(spec, pos);
if (hours === null)
return null;
const fields = [hours[0]];
pos = hours[1];
while (fields.length < 3 && spec.startsWith(':', pos)) {
const more = numberField(spec, pos + 1);
if (more === null)
break;
fields.push(more[0]);
pos = more[1];
}
const [hh = 0, mm = 0, ss = 0] = fields;
return [[hh, mm, ss], pos];
}
/**
* An abbreviation: three or more ASCII letters, or three or more of
* letters, digits, `+` and `-` between `<` and `>`. Returns the name and the
* position after it, or null when there is no name GNU accepts there.
*/
function zoneName(spec, pos) {
if (spec.startsWith('<', pos)) {
const stop = spanOf(spec, pos + 1, QUOTABLE);
if (stop - pos - 1 < 3 || !spec.startsWith('>', stop))
return null;
return [spec.slice(pos + 1, stop), stop + 1];
}
const stop = spanOf(spec, pos, LETTERS);
if (stop - pos < 3)
return null;
return [spec.slice(pos, stop), stop];
}
/**
* An offset `[+-]hh[:mm[:ss]]`, as seconds east of UTC (POSIX counts west,
* so `EST5` is -05:00). GNU caps the hours at 24 and the minutes and
* seconds at 59 (`UTC5:99` is -05:59, `UTC24:30` -24:30). Returns null for
* the offset when no hours follow the optional sign, and the position after
* what was read (a sign is read even then).
*/
function utcOffset(spec, pos) {
const west = !spec.startsWith('-', pos);
if (pos < spec.length && SIGNS.includes(spec.charAt(pos)))
pos++;
const clock = clockFields(spec, pos);
if (clock === null)
return [null, pos];
const [[hours, minutes, seconds], stop] = clock;
const total = Math.min(hours, 24) * HOUR + Math.min(minutes, 59) * 60 + Math.min(seconds, 59);
return [west ? -total : total, stop];
}
/**
* The `/time` after a rule's day, which is 02:00 when absent. The time takes
* a leading `-` and no caps (`/-1`, `/25`, `/3:99`). What GNU 9.7 does with
* a broken one, measured: `/` at the end of the string is midnight and
* refused; a `/` with no hours after it is 02:00 (`-02:00` after `/-`),
* accepted only when a comma or the end follows; any other character after
* the day is midnight and refused. Returns the rule with its time, the
* position after it, and whether GNU accepted it.
*/
function ruleTime(spec, pos, rule) {
if (pos === spec.length || spec[pos] === ',')
return [{ ...rule, seconds: 2 * HOUR }, pos, true];
if (spec[pos] !== '/' || pos + 1 === spec.length)
return [rule, pos, false];
pos++;
const sign = spec[pos] === '-' ? -1 : 1;
if (sign < 0)
pos++;
const clock = clockFields(spec, pos);
let seconds = 2 * HOUR;
if (clock !== null) {
const [[hours, minutes, secs], stop] = clock;
seconds = hours * HOUR + minutes * 60 + secs;
pos = stop;
}
const accepted = pos === spec.length || spec[pos] === ',';
return [{ ...rule, seconds: sign * seconds }, pos, accepted];
}
/**
* The day of a `Jn` (1 to 365) or `n` (0 to 365) rule: plain digits, no
* blank or sign; one out of range, or none, is refused as day 0. Returns the
* rule at midnight, the position after the digits, and whether the day was
* accepted.
*/
function yearDayRule(spec, pos, kind, first) {
const stop = spanOf(spec, pos, DIGITS);
const digits = spec.slice(pos, stop).replace(/^0+/, '');
const day = digits.length <= 3 ? Number(digits) : 1000;
if (stop === pos || day < first || day > 365)
return [{ ...UNREAD_RULE, kind }, stop, false];
return [{ ...UNREAD_RULE, kind, day }, stop, true];
}
/**
* The `m.w.d` of an `Mm.w.d` rule. A refused rule keeps all three fields as
* written: `M3.6.3` is the last Wednesday of March, `M3.5.9` the last
* Tuesday, `M3` the first Sunday. A month outside 1 to 12 gives GNU dates
* that follow no rule we measured; mirage reads that rule as Julian day 0.
* Returns the rule at midnight, the position after it, and whether all three
* fields were in range.
*/
function weekdayRule(spec, pos) {
const fields = [];
while (fields.length < 3) {
if (fields.length > 0 && !spec.startsWith('.', pos))
break;
const got = numberField(spec, pos + (fields.length > 0 ? 1 : 0));
if (got === null)
break;
fields.push(got[0]);
pos = got[1];
}
const [month = 0, week = 0, weekday = 0] = fields;
if (month < 1 || month > 12)
return [DAY_BEFORE_YEAR, pos, false];
const rule = { kind: 'M', month, week, weekday, day: 0, seconds: 0 };
const accepted = fields.length === 3 && week >= 1 && week <= 5 && weekday <= 6;
return [rule, pos, accepted];
}
/**
* One rule, `Mm.w.d`, `Jn` or `n`, with its time. Returns the rule, the
* position after it, and whether GNU accepted it; a refused rule keeps what
* was read of it.
*/
function dstTransition(spec, pos) {
const lead = spec.slice(pos, pos + 1);
let read;
if (lead === 'M')
read = weekdayRule(spec, pos + 1);
else if (lead === 'J')
read = yearDayRule(spec, pos + 1, 'J', 1);
else if (lead !== '' && DIGITS.includes(lead))
read = yearDayRule(spec, pos, 'D', 0);
else
return [UNREAD_RULE, pos, false];
const [rule, stop, accepted] = read;
if (!accepted)
return [rule, stop, false];
return ruleTime(spec, stop, rule);
}
/**
* The `,start[,end]` that follows the DST name and offset. With nothing
* there, or only a comma, both rules fall back; with only a start, the end
* does. A refused start leaves the end unread, and text other than a comma
* leaves both unread, which puts DST all year east of UTC
* (`CET-1CEST,bogus`) and nearly all year west of it.
*/
function dstRules(spec, pos) {
if (pos === spec.length || spec.slice(pos) === ',')
return [FALLBACK_START, FALLBACK_END];
if (spec[pos] !== ',')
return [UNREAD_RULE, UNREAD_RULE];
const [start, stop, accepted] = dstTransition(spec, pos + 1);
if (!accepted)
return [start, UNREAD_RULE];
if (stop + 1 >= spec.length)
return [start, FALLBACK_END];
return [start, dstTransition(spec, stop + 1)[0]];
}
// An offset in seconds east, held to what the Python twin's tzinfo may return.
function clamped(east) {
return Math.max(-OFFSET_LIMIT, Math.min(OFFSET_LIMIT, east));
}
/**
* The zone a POSIX TZ string describes, as GNU 9.7 renders it:
* `std offset [dst [offset] [,start[/time],end[/time]]]`. No DST name gives
* a fixed zone, and a DST offset defaults to an hour east of the standard
* one. A string GNU cannot read keeps what it read: no standard name is UTC
* with an empty name (`AB5`), no standard offset UTC under the name (`XYZ`,
* `Bogus/Zone`), and a DST name it cannot read a nameless half at UTC
* (`EST5x`).
*/
export function posixZone(spec) {
const std = zoneName(spec, 0);
if (std === null)
return new FixedZone(0, '');
const [stdName, afterName] = std;
const lead = spec.slice(afterName, afterName + 1);
if (lead === '' || !`${SIGNS}${DIGITS}`.includes(lead))
return new FixedZone(0, stdName);
const [stdEast, afterOffset] = utcOffset(spec, afterName);
if (stdEast === null)
return new FixedZone(0, stdName);
if (afterOffset === spec.length)
return new FixedZone(clamped(stdEast), stdName);
let pos = afterOffset;
let dstName = '';
let dstEast = 0;
const dst = zoneName(spec, pos);
if (dst !== null) {
let offset;
[dstName, pos] = dst;
[offset, pos] = utcOffset(spec, pos);
dstEast = offset ?? stdEast + HOUR;
}
const [start, end] = dstRules(spec, pos);
return new PosixZone(stdName, clamped(stdEast), dstName, clamped(dstEast), start, end);
}
//# sourceMappingURL=timezone.js.map