Skip to content

Commit 622fdd3

Browse files
refactor: update XO and modernize codebase while adopting new rules (#520)
1 parent 20c24d5 commit 622fdd3

17 files changed

Lines changed: 2721 additions & 3873 deletions

build/update-windows-zones.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,13 @@ function mergeLegacyOverrides(zoneTable, oldMap) {
117117

118118
const unresolved = [];
119119
let merged = 0;
120-
for (const key of Object.keys(oldMap)) {
121-
const iana = getFirstIanaFromLookup(zoneTable, oldMap[key]);
120+
for (const [key, windowsId] of Object.entries(oldMap)) {
121+
const iana = getFirstIanaFromLookup(zoneTable, windowsId);
122122
if (iana) {
123123
zoneTable[key] = {iana: [iana]};
124124
merged++;
125125
} else {
126-
unresolved.push({label: key, windowsId: oldMap[key]});
126+
unresolved.push({label: key, windowsId});
127127
}
128128
}
129129

@@ -132,6 +132,7 @@ function mergeLegacyOverrides(zoneTable, oldMap) {
132132

133133
function writeOutput(filePath, data) {
134134
// Deterministic, diff-friendly: one top-level key per line, minified values, sorted keys
135+
// eslint-disable-next-line unicorn/require-array-sort-compare -- plain string keys; default lexicographic toSorted() is sufficient and clearer here
135136
const keys = Object.keys(data).toSorted();
136137
const lines = ['{'];
137138
for (const [idx, key] of keys.entries()) {

examples/example-rrule-dayjs.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ for (const event of events) {
5353
const title = instance.summary;
5454
const startDate = dayjs(instance.start);
5555
const endDate = dayjs(instance.end);
56-
const duration = dayjs.duration(endDate.diff(startDate));
56+
const eventDuration = dayjs.duration(endDate.diff(startDate));
5757

5858
console.log(`title:${title}`);
5959
console.log(`startDate:${startDate.format('LLLL')}`);
6060
console.log(`endDate:${endDate.format('LLLL')}`);
61-
console.log(`duration:${Math.floor(duration.asHours())}:${String(duration.minutes()).padStart(2, '0')} hours`);
61+
console.log(`duration:${Math.floor(eventDuration.asHours())}:${String(eventDuration.minutes()).padStart(2, '0')} hours`);
6262
console.log();
6363
}
6464
}

ical.js

Lines changed: 85 additions & 76 deletions
Large diffs are not rendered by default.

lib/tz-utils.js

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const Temporal = globalThis.Temporal || require('temporal-polyfill').Temporal;
66
const windowsZones = require('../windowsZones.json');
77

88
// Ensure polyfill is globally available for downstream modules
9+
// eslint-disable-next-line unicorn/no-global-object-property-assignment -- shared polyfill bootstrap keeps runtime behavior explicit
910
globalThis.Temporal ??= Temporal;
1011

1112
// Minimal alias map to emulate the subset of moment.tz.link behavior tests rely on
@@ -16,7 +17,7 @@ const aliasMap = new Map();
1617
* Collapses whitespace, trims the result, and lowercases the value for case-insensitive lookups.
1718
*
1819
* @param {string} label
19-
* @returns {string}
20+
* @returns {string} Normalized label
2021
*/
2122
function normalizeWindowsLabel(label) {
2223
return String(label)
@@ -30,7 +31,7 @@ function normalizeWindowsLabel(label) {
3031
* This lets us resolve the canonical IANA identifier without relying on fuzzy substring matching.
3132
*
3233
* @param {Record<string, {iana: string[]}>} source
33-
* @returns {Map<string, {iana: string[]}>}
34+
* @returns {Map<string, {iana: string[]}>} Index map of normalized labels to data
3435
*/
3536
function buildWindowsLabelIndex(source) {
3637
const index = new Map();
@@ -68,7 +69,7 @@ const windowsLabelIndex = buildWindowsLabelIndex(windowsZones);
6869
* Resolve a Windows/legacy timezone label to the canonical IANA identifier exported in windowsZones.json.
6970
*
7071
* @param {string} label
71-
* @returns {string|null}
72+
* @returns {string|null} IANA zone or null if not found
7273
*/
7374
function mapWindowsZone(label) {
7475
const exact = windowsZones[label];
@@ -106,7 +107,7 @@ const validIanaCache = new Map();
106107
* Convert textual UTC offsets ("+05:30", "UTC-4", "(UTC+02:00)") into signed minute counts.
107108
*
108109
* @param {string} offset
109-
* @returns {number|undefined}
110+
* @returns {number|undefined} Total minutes or undefined if invalid
110111
*/
111112
function offsetLabelToMinutes(offset) {
112113
if (!offset) {
@@ -118,7 +119,7 @@ function offsetLabelToMinutes(offset) {
118119
.replace(/^\(?(?:utc|gmt)\)?\s*/iv, '')
119120
.replace(/\)$/v, '')
120121
.trim();
121-
const match = trimmed.match(/^([+\-])(\d{1,2})(?::?(\d{2}))?$/v);
122+
const match = /^([+\-])(\d{1,2})(?::?(\d{2}))?$/v.exec(trimmed);
122123
if (!match) {
123124
return undefined;
124125
}
@@ -177,7 +178,7 @@ function minutesToEtcZone(totalMinutes) {
177178
* Interpret a TZID value (IANA, Windows display name, or offset label) and return structured metadata.
178179
*
179180
* @param {string} value
180-
* @returns {{original: string|undefined, iana: string|undefined, offset: string|undefined, offsetMinutes: number|undefined, etc: string|undefined}}
181+
* @returns {{original: string|undefined, iana: string|undefined, offset: string|undefined, offsetMinutes: number|undefined, etc: string|undefined}} Resolved timezone metadata
181182
*/
182183
function resolveTZID(value) {
183184
if (typeof value !== 'string' || value.length === 0) {
@@ -247,7 +248,7 @@ function resolveTZID(value) {
247248
*
248249
* @param {Date} date
249250
* @param {{iana?: string, offsetMinutes?: number}} tzInfo
250-
* @returns {string|undefined}
251+
* @returns {string|undefined} Formatted wall-time string or undefined if invalid
251252
*/
252253
function formatDateForRrule(date, tzInfo = {}) {
253254
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
@@ -276,7 +277,7 @@ function formatDateForRrule(date, tzInfo = {}) {
276277
*
277278
* @param {Date} date
278279
* @param {string|undefined} tzid
279-
* @returns {Date|undefined}
280+
* @returns {Date|undefined} Date with attached timezone metadata
280281
*/
281282
function attachTz(date, tzid) {
282283
if (!date || !tzid) {
@@ -314,6 +315,8 @@ function guessLocalZone() {
314315
*
315316
* We depend on Node 20+, so `Intl.supportedValuesOf('timeZone')` is guaranteed
316317
* to exist and yields the canonical list without requiring extra guards.
318+
*
319+
* @returns {string[]} Array of IANA timezone names
317320
*/
318321
function getZoneNames() {
319322
return Intl.supportedValuesOf('timeZone');
@@ -362,7 +365,7 @@ function parseDateTimeInZone(yyyymmddThhmmss, zone) {
362365
const s = String(yyyymmddThhmmss);
363366
// Support basic and extended forms
364367
// Try extended first: YYYY-MM-DDTHH:mm:ss
365-
let m = s.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/v);
368+
let m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/v.exec(s);
366369
let fields;
367370
if (m) {
368371
fields = {
@@ -375,7 +378,7 @@ function parseDateTimeInZone(yyyymmddThhmmss, zone) {
375378
};
376379
} else {
377380
// Basic form: YYYYMMDDTHHmmss or YYYYMMDDTHHmm
378-
m = s.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})?$/v);
381+
m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})?$/v.exec(s);
379382
if (m) {
380383
fields = {
381384
year: Number(m[1]),
@@ -413,10 +416,10 @@ function parseDateTimeInZone(yyyymmddThhmmss, zone) {
413416
function parseWithOffset(yyyymmddThhmmss, offset) {
414417
// Offset like +hh:mm, -hh:mm, +hhmm, -hhmm, optionally prefixed by UTC/GMT
415418
const s = String(yyyymmddThhmmss);
416-
let m = s.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})?$/v);
419+
let m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})?$/v.exec(s);
417420
// Some feeds emit extended ISO `YYYY-MM-DD[T ]HH:mm[:ss]` strings alongside numeric offsets.
418421
// Mirror parseDateTimeInZone by accepting that form too so we don't fall back to local Date semantics.
419-
m ||= s.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/v);
422+
m ||= /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/v.exec(s);
420423

421424
if (!m) {
422425
return undefined;
@@ -484,7 +487,7 @@ const vtimezoneIanaCache = new Map();
484487
*
485488
* @param {Array} blocks - Array of STANDARD or DAYLIGHT components (all same type)
486489
* @param {number} refYear - The event year to look up the rule for
487-
* @returns {Object|undefined}
490+
* @returns {object | undefined} Applicable observance block or undefined
488491
*/
489492
function pickApplicableBlock(blocks, refYear) {
490493
if (blocks.length === 0) {
@@ -527,10 +530,10 @@ function pickApplicableBlock(blocks, refYear) {
527530
* No reliable representation is available; callers should fall back to
528531
* floating/local time rather than returning a confidently wrong offset.
529532
*
530-
* @param {Object} vTimezone - Parsed VTIMEZONE object (from the node-ical parser stack)
533+
* @param {object} vTimezone - Parsed VTIMEZONE object (from the node-ical parser stack)
531534
* @param {number} year - Reference year used to select the applicable observance block
532535
* and to probe the IANA database (DST boundaries can change historically).
533-
* @returns {{ iana: string|undefined, offset: string|undefined }}
536+
* @returns {{ iana: string|undefined, offset: string|undefined }} IANA zone and/or fixed offset
534537
*/
535538
function resolveVTimezoneToIana(vTimezone, year) {
536539
if (!vTimezone || typeof vTimezone !== 'object') {
@@ -540,7 +543,7 @@ function resolveVTimezoneToIana(vTimezone, year) {
540543
// Reject unusable year values before they can corrupt the cache key or cause
541544
// Temporal.Instant.from() to throw on a malformed ISO string.
542545
const yearNumber = Number(year);
543-
if (!Number.isFinite(yearNumber) || !Number.isInteger(yearNumber)) {
546+
if (!Number.isFinite(yearNumber) || !Number.isSafeInteger(yearNumber)) {
544547
return {iana: undefined, offset: undefined};
545548
}
546549

@@ -645,5 +648,5 @@ function isUtcTimezone(tz) {
645648
}
646649

647650
const tzLower = tz.toLowerCase();
648-
return tzLower === 'etc/utc' || tzLower === 'utc' || tzLower === 'etc/gmt';
651+
return ['etc/utc', 'utc', 'etc/gmt'].includes(tzLower);
649652
}

node-ical.d.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
13
declare module 'node-ical' {
24
/**
35
* Compatibility wrapper returned by node-ical (RRULE results are Date-based).
@@ -45,7 +47,10 @@ declare module 'node-ical' {
4547
* Methods (Async)
4648
*/
4749
export type NodeICalAsync = {
48-
fromURL: ((url: string, callback: NodeIcalCallback) => void) & ((url: string, options: FetchOptions | NodeIcalCallback, callback?: NodeIcalCallback) => void) & ((url: string) => Promise<CalendarResponse>);
50+
fromURL:
51+
((url: string, callback: NodeIcalCallback) => void)
52+
& ((url: string, options: FetchOptions | NodeIcalCallback, callback?: NodeIcalCallback) => void)
53+
& ((url: string) => Promise<CalendarResponse>);
4954

5055
parseICS: ((body: string, callback: NodeIcalCallback) => void) & ((body: string) => Promise<CalendarResponse>);
5156

@@ -161,11 +166,11 @@ declare module 'node-ical' {
161166

162167
export type VTimeZone = TimeZoneProps & TimeZoneDictionary;
163168

164-
type TimeZoneProps = {
169+
type TimeZoneProps = BaseComponent & {
165170
type: 'VTIMEZONE';
166171
tzid: string;
167172
tzurl?: string;
168-
} & BaseComponent;
173+
};
169174

170175
type TimeZoneDictionary = Record<string, TimeZoneDef | undefined>;
171176

@@ -180,7 +185,7 @@ declare module 'node-ical' {
180185
/**
181186
* https://www.kanzaki.com/docs/ical/valarm.html
182187
*/
183-
export type VAlarm = {
188+
export type VAlarm = BaseComponent & {
184189
type: 'VALARM';
185190
action: 'AUDIO' | 'DISPLAY' | 'EMAIL' | 'PROCEDURE';
186191
trigger: Trigger;
@@ -209,7 +214,7 @@ declare module 'node-ical' {
209214
*/
210215
attendee?: Attendee;
211216

212-
} & BaseComponent;
217+
};
213218

214219
/**
215220
* Common properties shared by calendar components (VEVENT, VTODO, VJOURNAL)
@@ -235,7 +240,7 @@ declare module 'node-ical' {
235240
exdate?: Record<string, DateWithTimeZone>;
236241
};
237242

238-
export type VEvent = CalendarComponentCommon & {
243+
export type VEvent = CalendarComponentCommon & BaseComponent & {
239244
type: 'VEVENT';
240245
// RFC 5545 required fields (override optional from CalendarComponentCommon)
241246
uid: string;
@@ -267,7 +272,7 @@ declare module 'node-ical' {
267272
*/
268273
recurrences?: Record<string, Omit<VEvent, 'recurrences'>>;
269274
alarms?: VAlarm[];
270-
} & BaseComponent;
275+
};
271276

272277
/**
273278
* Todo status values as defined in RFC 5545
@@ -291,7 +296,7 @@ declare module 'node-ical' {
291296
* console.log(`Completed: ${todo.completion}%`);
292297
* });
293298
*/
294-
export type VTodo = CalendarComponentCommon & {
299+
export type VTodo = CalendarComponentCommon & BaseComponent & {
295300
type: 'VTODO';
296301
// RFC 5545 required fields (override optional from CalendarComponentCommon)
297302
uid: string;
@@ -315,7 +320,7 @@ declare module 'node-ical' {
315320
*/
316321
recurrences?: Record<string, Omit<VTodo, 'recurrences'>>;
317322
alarms?: VAlarm[];
318-
} & BaseComponent;
323+
};
319324

320325
/**
321326
* VJOURNAL component representing a journal entry or note.
@@ -328,7 +333,7 @@ declare module 'node-ical' {
328333
* console.log(`Description: ${journal.description}`);
329334
* });
330335
*/
331-
export type VJournal = CalendarComponentCommon & {
336+
export type VJournal = CalendarComponentCommon & BaseComponent & {
332337
type: 'VJOURNAL';
333338
// RFC 5545 required fields (override optional from CalendarComponentCommon)
334339
uid: string;
@@ -341,7 +346,7 @@ declare module 'node-ical' {
341346
* Uses dual-key approach (date and ISO timestamp).
342347
*/
343348
recurrences?: Record<string, Omit<VJournal, 'recurrences'>>;
344-
} & BaseComponent;
349+
};
345350

346351
/**
347352
* Free/busy time type as defined in RFC 5545
@@ -374,7 +379,7 @@ declare module 'node-ical' {
374379
* });
375380
* }
376381
*/
377-
export type VFreebusy = {
382+
export type VFreebusy = BaseComponent & {
378383
type: 'VFREEBUSY';
379384
method?: Method;
380385
uid?: string;
@@ -391,7 +396,7 @@ declare module 'node-ical' {
391396
freebusy?: FreebusyPeriod[];
392397
/** Attendee information */
393398
attendee?: Attendee[] | Attendee;
394-
} & BaseComponent;
399+
};
395400

396401
/**
397402
* VCALENDAR component containing calendar-level metadata.

0 commit comments

Comments
 (0)