From 18e668517947d67c772aff0278ab9d5180f53741 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Wed, 11 Nov 2026 15:02:44 +0100 Subject: [PATCH 1/2] refactor: migrate calendar to ESM --- defaultmodules/calendar/calendar.js | 2 +- defaultmodules/calendar/calendarfetcher.js | 12 +++---- .../calendar/calendarfetcherutils.js | 10 +++--- .../{calendarutils.js => calendarutils.mjs} | 8 ++--- defaultmodules/calendar/debug.js | 12 ++++--- defaultmodules/calendar/node_helper.js | 10 +++--- defaultmodules/calendar/package.json | 3 ++ eslint.config.mjs | 8 ++--- .../default/calendar/calendar_fetcher_spec.js | 16 +++++----- .../calendar/calendar_fetcher_utils_spec.js | 31 +++++++++---------- ...ule.js => calendar_fetcher_utils_spec.mjs} | 7 ++--- .../default/calendar/calendar_utils_spec.js | 7 ++--- vitest.config.mjs | 5 +-- 13 files changed, 62 insertions(+), 69 deletions(-) rename defaultmodules/calendar/{calendarutils.js => calendarutils.mjs} (96%) create mode 100644 defaultmodules/calendar/package.json rename tests/unit/modules/default/calendar/{calendar_fetcher_utils_bad_rrule.js => calendar_fetcher_utils_spec.mjs} (81%) diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index d09f9d53f2..7885b59751 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -75,7 +75,7 @@ Module.register("calendar", { // Define required scripts. getScripts () { - return ["calendarutils.js", "moment.js", "moment-timezone.js"]; + return ["calendarutils.mjs", "moment.js", "moment-timezone.js"]; }, // Define required translations. diff --git a/defaultmodules/calendar/calendarfetcher.js b/defaultmodules/calendar/calendarfetcher.js index f7bfe5634b..c7dba81c2b 100644 --- a/defaultmodules/calendar/calendarfetcher.js +++ b/defaultmodules/calendar/calendarfetcher.js @@ -1,7 +1,7 @@ -const ical = require("node-ical"); -const Log = require("logger"); -const CalendarFetcherUtils = require("./calendarfetcherutils"); -const HTTPFetcher = require("#http_fetcher"); +import { async as icalAsync } from "node-ical"; +import Log from "logger"; +import HTTPFetcher from "../../js/http_fetcher.mjs"; +import { CalendarFetcherUtils } from "./calendarfetcherutils.js"; /** * CalendarFetcher - Fetches and parses iCal calendar data @@ -66,7 +66,7 @@ class CalendarFetcher { maximumNumberOfDays: this.maximumNumberOfDays }); - const parsed = await ical.async.parseICS(filteredData); + const parsed = await icalAsync.parseICS(filteredData); Log.debug(`Parsed iCal data from ${this.url} with ${Object.keys(parsed).length} entries.`); @@ -139,4 +139,4 @@ class CalendarFetcher { } } -module.exports = CalendarFetcher; +export { CalendarFetcher }; diff --git a/defaultmodules/calendar/calendarfetcherutils.js b/defaultmodules/calendar/calendarfetcherutils.js index 5710a79d7e..28e90677c7 100644 --- a/defaultmodules/calendar/calendarfetcherutils.js +++ b/defaultmodules/calendar/calendarfetcherutils.js @@ -1,10 +1,10 @@ /** * @external Moment */ -const moment = require("moment-timezone"); -const ical = require("node-ical"); +import moment from "moment-timezone"; +import ical from "node-ical"; -const Log = require("logger"); +import Log from "logger"; const CalendarFetcherUtils = { @@ -341,6 +341,4 @@ const CalendarFetcherUtils = { } }; -if (typeof module !== "undefined") { - module.exports = CalendarFetcherUtils; -} +export { CalendarFetcherUtils }; diff --git a/defaultmodules/calendar/calendarutils.js b/defaultmodules/calendar/calendarutils.mjs similarity index 96% rename from defaultmodules/calendar/calendarutils.js rename to defaultmodules/calendar/calendarutils.mjs index 7128f0cfe1..2c9dbb891d 100644 --- a/defaultmodules/calendar/calendarutils.js +++ b/defaultmodules/calendar/calendarutils.mjs @@ -25,7 +25,7 @@ const CalendarUtils = { return { longDateFormat: { LT: "HH:mm" } }; } default: { - return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } }; + return { longDateFormat: { LT: globalThis.moment.localeData().longDateFormat("LT") } }; } } }, @@ -131,6 +131,6 @@ const CalendarUtils = { } }; -if (typeof module !== "undefined") { - module.exports = CalendarUtils; -} +globalThis.CalendarUtils = CalendarUtils; + +export default CalendarUtils; diff --git a/defaultmodules/calendar/debug.js b/defaultmodules/calendar/debug.js index 30abbd8d13..9a8770ddae 100644 --- a/defaultmodules/calendar/debug.js +++ b/defaultmodules/calendar/debug.js @@ -3,11 +3,13 @@ * use this script with `node debug.js` to test the fetcher without the need * of starting the MagicMirror² core. Adjust the values below to your desire. */ -// Load internal alias resolver -require("../../js/alias-resolver"); -const Log = require("logger"); +import { createRequire } from "node:module"; -const CalendarFetcher = require("./calendarfetcher"); +const require = createRequire(import.meta.url); +require("../../js/alias-resolver.js"); + +const { default: Log } = await import("logger"); +const { CalendarFetcher } = await import("./calendarfetcher.js"); const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL //const url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first) @@ -35,6 +37,6 @@ fetcher.onError((fetcher, error) => { process.exit(1); }); -fetcher.startFetch(); +fetcher.fetchCalendar(); Log.log("Create fetcher done! "); diff --git a/defaultmodules/calendar/node_helper.js b/defaultmodules/calendar/node_helper.js index de69e4e075..c1da4c4e7e 100644 --- a/defaultmodules/calendar/node_helper.js +++ b/defaultmodules/calendar/node_helper.js @@ -1,9 +1,9 @@ -const zlib = require("node:zlib"); -const NodeHelper = require("node_helper"); -const Log = require("logger"); -const CalendarFetcher = require("./calendarfetcher"); +import zlib from "node:zlib"; +import NodeHelper from "node_helper"; +import Log from "logger"; +import { CalendarFetcher } from "./calendarfetcher.js"; -module.exports = NodeHelper.create({ +export default NodeHelper.create({ // Override start method. start () { Log.log(`Starting node helper for: ${this.name}`); diff --git a/defaultmodules/calendar/package.json b/defaultmodules/calendar/package.json new file mode 100644 index 0000000000..bedb411a91 --- /dev/null +++ b/defaultmodules/calendar/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 567e06c77b..2bc0f24e9b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -32,7 +32,7 @@ export default defineConfig([ } }, { - files: ["**/*.js", "defaultmodules/defaultmodules.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"], + files: ["**/*.js", "defaultmodules/defaultmodules.mjs", "defaultmodules/calendar/calendarutils.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"], languageOptions: { ecmaVersion: "latest", globals: { @@ -104,7 +104,7 @@ export default defineConfig([ } }, { - files: ["**/*.js", "defaultmodules/defaultmodules.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"], + files: ["**/*.js", "defaultmodules/defaultmodules.mjs", "defaultmodules/calendar/calendarutils.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"], ignores: [ "clientonly/index.js", "js/logger.js", @@ -119,7 +119,7 @@ export default defineConfig([ }, { files: ["**/*.mjs"], - ignores: ["defaultmodules/defaultmodules.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"], + ignores: ["defaultmodules/defaultmodules.mjs", "defaultmodules/calendar/calendarutils.mjs", "js/app.mjs", "js/deprecated.mjs", "js/electron_helper.mjs", "js/http_fetcher.mjs", "js/ip_access_control.mjs", "js/releasenotes.mjs", "js/server.mjs", "js/server_functions.mjs", "js/vendor.mjs", "tests/**/*.mjs"], languageOptions: { ecmaVersion: "latest", globals: { @@ -181,7 +181,7 @@ export default defineConfig([ } }, { - files: ["tests/mocks/**/node_helper.js", "defaultmodules/*/node_helper.js", "defaultmodules/*/git_helper.js", "defaultmodules/newsfeed/feeditem.js", "defaultmodules/newsfeed/newsfeedfetcher.js"], + files: ["tests/mocks/**/node_helper.js", "defaultmodules/*/node_helper.js", "defaultmodules/*/git_helper.js", "defaultmodules/newsfeed/feeditem.js", "defaultmodules/newsfeed/newsfeedfetcher.js", "defaultmodules/calendar/calendarfetcher.js", "defaultmodules/calendar/calendarfetcherutils.js", "defaultmodules/calendar/debug.js"], rules: { // MagicMirror aliases are resolved at runtime by js/alias-resolver.js. "import-x/no-unresolved": ["error", {ignore: ["^node_helper$", "^logger$"]}] diff --git a/tests/unit/modules/default/calendar/calendar_fetcher_spec.js b/tests/unit/modules/default/calendar/calendar_fetcher_spec.js index e094db28b4..f7784f165d 100644 --- a/tests/unit/modules/default/calendar/calendar_fetcher_spec.js +++ b/tests/unit/modules/default/calendar/calendar_fetcher_spec.js @@ -1,12 +1,10 @@ -global.moment = require("moment-timezone"); +import { afterEach, describe, expect, it, vi } from "vitest"; +import { async as icalAsync } from "node-ical"; +import moment from "moment-timezone"; -const ical = require("node-ical"); -const moment = require("moment-timezone"); -const defaults = require("../../../../../js/defaults"); - -const CalendarFetcherUtils = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarfetcherutils`); - -const CalendarFetcher = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarfetcher`); +globalThis.moment = moment; +const { CalendarFetcherUtils } = await import("../../../../../defaultmodules/calendar/calendarfetcherutils.js"); +const { CalendarFetcher } = await import("../../../../../defaultmodules/calendar/calendarfetcher.js"); const makeFetcher = (options = {}) => new CalendarFetcher( options.url ?? "http://test.example.com/cal.ics", @@ -78,7 +76,7 @@ describe("CalendarFetcher", () => { await emitResponse(fetcher, new Response(futureEventICS(), { status: 200 })); expect(fetcher.events).toHaveLength(1); - vi.spyOn(ical.async, "parseICS").mockRejectedValueOnce(new Error("invalid ics")); + vi.spyOn(icalAsync, "parseICS").mockRejectedValueOnce(new Error("invalid ics")); const error = await emitResponse(fetcher, new Response("BROKEN", { status: 200 })); expect(fetcher.events).toHaveLength(1); diff --git a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js index a61017f41b..5519017444 100644 --- a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js +++ b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js @@ -1,10 +1,7 @@ -global.moment = require("moment-timezone"); - -const ical = require("node-ical"); -const moment = require("moment-timezone"); -const defaults = require("../../../../../js/defaults"); - -const CalendarFetcherUtils = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarfetcherutils`); +import { describe, expect, it } from "vitest"; +import { parseICS } from "node-ical"; +import moment from "moment-timezone"; +import { CalendarFetcherUtils } from "../../../../../defaultmodules/calendar/calendarfetcherutils.js"; describe("Calendar fetcher utils test", () => { const defaultConfig = { @@ -114,7 +111,7 @@ describe("Calendar fetcher utils test", () => { }); it("should return the correct times when recurring events pass through daylight saving time", () => { - const data = ical.parseICS(`BEGIN:VEVENT + const data = parseICS(`BEGIN:VEVENT DTSTART;TZID=Europe/Amsterdam:20250311T090000 DTEND;TZID=Europe/Amsterdam:20250311T091500 RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE,SA,SU @@ -148,7 +145,7 @@ END:VEVENT`); }); it("should return the correct moments based on the timezone given", () => { - const data = ical.parseICS(`BEGIN:VEVENT + const data = parseICS(`BEGIN:VEVENT DTSTART;TZID=Europe/Amsterdam:20250311T090000 DTEND;TZID=Europe/Amsterdam:20250311T091500 RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE,SA,SU @@ -170,7 +167,7 @@ END:VEVENT`); it("should return correct day-of-week for full-day recurring events across DST transitions", () => { // Test case for GitHub issue #3976: recurring full-day events showing on wrong day // This happens when DST transitions change the UTC offset between occurrences - const data = ical.parseICS(`BEGIN:VCALENDAR + const data = parseICS(`BEGIN:VCALENDAR BEGIN:VEVENT DTSTART;VALUE=DATE:20251027 DTEND;VALUE=DATE:20251028 @@ -210,7 +207,7 @@ END:VCALENDAR`); // Facebook birthday calendars use DTSTART with the actual birth year (e.g. 1990), // which previously caused rrule.js to return the wrong year occurrence. // With rrule-temporal this works correctly without any special-casing. - const data = ical.parseICS(`BEGIN:VCALENDAR + const data = parseICS(`BEGIN:VCALENDAR BEGIN:VEVENT DTSTART;VALUE=DATE:19900215 RRULE:FREQ=YEARLY @@ -277,7 +274,7 @@ END:VCALENDAR`); // node-ical creates DATE-only events with the local Date constructor: new Date(year, month, day). // getFullYear() on a locally-constructed date always returns the correct calendar year // regardless of the server's UTC offset — guard against regressions that switch to getUTCFullYear(). - const data = ical.parseICS(`BEGIN:VCALENDAR + const data = parseICS(`BEGIN:VCALENDAR BEGIN:VEVENT DTSTART;VALUE=DATE:19900101 DTEND;VALUE=DATE:19900102 @@ -324,7 +321,7 @@ END:VCALENDAR`); describe("expandRecurringEvent", () => { it("should extend end to end-of-day when event has no DTEND", () => { // node-ical sets end === start when DTEND is absent; our code extends to endOf("day") - const data = ical.parseICS(`BEGIN:VCALENDAR + const data = parseICS(`BEGIN:VCALENDAR BEGIN:VEVENT DTSTART:20260222T100000Z UID:no-end-test@test @@ -340,7 +337,7 @@ END:VCALENDAR`); it("should apply RECURRENCE-ID overrides (moved single occurrence)", () => { // A weekly event on Mondays at 10:00, but the second occurrence is moved to Tuesday 14:00 - const data = ical.parseICS(`BEGIN:VCALENDAR + const data = parseICS(`BEGIN:VCALENDAR BEGIN:VEVENT DTSTART;TZID=Europe/Berlin:20260302T100000 DTEND;TZID=Europe/Berlin:20260302T110000 @@ -379,7 +376,7 @@ END:VCALENDAR`); it("should handle events with DURATION instead of DTEND", () => { // RFC 5545 allows DURATION as alternative to DTEND - const data = ical.parseICS(`BEGIN:VCALENDAR + const data = parseICS(`BEGIN:VCALENDAR BEGIN:VEVENT DTSTART:20260315T090000Z DURATION:PT1H30M @@ -401,7 +398,7 @@ END:VCALENDAR`); }); it("should handle recurring events with DURATION instead of DTEND", () => { - const data = ical.parseICS(`BEGIN:VCALENDAR + const data = parseICS(`BEGIN:VCALENDAR BEGIN:VEVENT DTSTART;TZID=Europe/Berlin:20260301T080000 DURATION:PT45M @@ -528,7 +525,7 @@ END:VCALENDAR`); const yearConfig = { ...defaultConfig, maximumNumberOfDays: 365 }; const buildEvent = (rrule, dtstart = "20231002", dtend = "20231003") => { - return ical.parseICS(`BEGIN:VCALENDAR + return parseICS(`BEGIN:VCALENDAR BEGIN:VEVENT DTSTART;VALUE=DATE:${dtstart} DTEND;VALUE=DATE:${dtend} diff --git a/tests/unit/modules/default/calendar/calendar_fetcher_utils_bad_rrule.js b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.mjs similarity index 81% rename from tests/unit/modules/default/calendar/calendar_fetcher_utils_bad_rrule.js rename to tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.mjs index 6b4a8dc651..5121b5f4d1 100644 --- a/tests/unit/modules/default/calendar/calendar_fetcher_utils_bad_rrule.js +++ b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.mjs @@ -1,8 +1,7 @@ -global.moment = require("moment-timezone"); +import moment from "moment-timezone"; +import { CalendarFetcherUtils } from "../../../../../defaultmodules/calendar/calendarfetcherutils.js"; -const defaults = require("../../../js/defaults"); - -const CalendarFetcherUtils = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarfetcherutils`); +globalThis.moment = moment; describe("Calendar fetcher utils test", () => { const defaultConfig = { diff --git a/tests/unit/modules/default/calendar/calendar_utils_spec.js b/tests/unit/modules/default/calendar/calendar_utils_spec.js index 3c99f9ce6e..88e7649df2 100644 --- a/tests/unit/modules/default/calendar/calendar_utils_spec.js +++ b/tests/unit/modules/default/calendar/calendar_utils_spec.js @@ -1,8 +1,7 @@ -global.moment = require("moment"); +import moment from "moment"; +import CalendarUtils from "../../../../../defaultmodules/calendar/calendarutils.mjs"; -const defaults = require("../../../../../js/defaults"); - -const CalendarUtils = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarutils`); +globalThis.moment = moment; describe("Calendar utils tests", () => { describe("capFirst", () => { diff --git a/vitest.config.mjs b/vitest.config.mjs index 402de4b39e..af069ff845 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -59,10 +59,7 @@ export default defineConfig({ globals: true, environment: "node", setupFiles: ["./tests/utils/vitest-setup.js"], - include: [ - "tests/unit/**/*_spec.{js,mjs}", - "tests/unit/defaultmodules/calendar/calendar_fetcher_utils_bad_rrule.js" - ], + include: ["tests/unit/**/*_spec.{js,mjs}"], testTimeout: 20000, hookTimeout: 10000 } From 2e6d8061595c0de7528427f62ac08873cb7666e5 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:03:51 +0100 Subject: [PATCH 2/2] ci: run tests on ubuntu-26.04 and add apt step timeout --- .github/workflows/automated-tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/automated-tests.yaml b/.github/workflows/automated-tests.yaml index 0abd9686b5..b69a693880 100644 --- a/.github/workflows/automated-tests.yaml +++ b/.github/workflows/automated-tests.yaml @@ -54,13 +54,14 @@ jobs: run: | node --run install-mm:dev test: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 30 strategy: matrix: node-version: [22.x, 24.x, 26.x] steps: - name: Install electron dependencies and labwc + timeout-minutes: 10 run: | sudo apt-get update sudo apt-get install -y libnss3 libasound2t64 labwc