Skip to content

feat: typescript migration and use yarn 4#4179

Draft
pierrejochem wants to merge 28 commits into
MagicMirrorOrg:masterfrom
pierrejochem:feat/typescript-migration
Draft

feat: typescript migration and use yarn 4#4179
pierrejochem wants to merge 28 commits into
MagicMirrorOrg:masterfrom
pierrejochem:feat/typescript-migration

Conversation

@pierrejochem
Copy link
Copy Markdown

Just a proposal for switching to typescript in combination with yarn 4

pierre-jochem and others added 28 commits June 4, 2026 13:44
Set up the foundation for migrating MagicMirror to strict TypeScript while
keeping the existing dual-runtime architecture intact (browser global scripts +
Node CommonJS), with zero changes to hardcoded .js runtime paths.

Approach: author .ts under src/, compile in place back into js/, defaultmodules/,
serveronly/, clientonly/, translations/ (outputs gitignored). No bundler — browser
files stay TS "script" files (no top-level import/export) so tsc emits global-
defining JS 1:1. Two project configs split the DOM (browser) vs node (server) libs.

- tsconfig.base.json + src/tsconfig.{browser,server}.json + editor tsconfig.json
- src/types/globals.d.ts (ambient window globals; promoted from module-types.ts)
  and browser-shims.d.ts for the dual CommonJS export guard
- build/build:watch/clean scripts; pre-build hooks on test*/server/start/config:check
  so the suite and runtime always execute fresh compiled output
- eslint: typescript-eslint block for src/**/*.ts mirroring the JS @Stylistic style;
  ignore compiled output; vitest esbuild target es2022
- *.ts formatted via eslint (tabs), added to .prettierignore; .ts in editorconfig tab group
- migrate js/deprecated.js -> src/js/deprecated.ts to validate the src->output mapping

Gate: full unit suite green (357 passing; the 1 failure is the pre-existing
macOS-only systeminformation test that asserts a Linux platform).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the leaf server/CommonJS modules and the dual-world defaultmodules/utils
to strict .ts under src/, compiled in place. Runtime behavior unchanged.

- js/utils, js/http_fetcher, js/server_functions, defaultmodules/utils -> src/*.ts
- server_functions now uses ES named exports (tsc emits exports.X under commonjs,
  so require("#server_functions") destructuring keeps working) so migrated callers
  can `import { getUserAgent } from "#server_functions"` with real resolution
- type internal aliases via ambient declares: src/types/aliases.d.ts (logger,
  node_helper -> any until migrated) and src/types/node-globals.d.ts (config,
  root_path, version, ...). #-subpath imports map to migrated src via tsconfig paths
- allowJs:false so tsc never pulls/overwrites hand-written .js inputs
- keep require()/module guards for untyped deps and dynamic requires; import typed
  packages (node builtins, undici) for real types; fix strict-mode issues
  (catch unknown, possibly-undefined env vars, non-null match, class field decls)
- eslint: ignore the 4 new compiled outputs; add src/**/*.d.ts override (declare var)

Gate: build clean, eslint/prettier clean, 357 unit tests pass (the 1 failure is the
pre-existing macOS-only systeminformation test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate 16 files via parallel translation, runtime behavior unchanged:
- 11 weather providers + weather/provider-utils + calendar/calendarfetcherutils
  -> server .ts (require()/export = kept; typed class fields; strict fixes)
- weatherobject, weatherutils, calendarutils -> browser dual-world .ts: kept as
  global script files (no top-level import/export) with the
  `if (typeof module !== "undefined") module.exports = X` guard intact, so both
  <script> loading and the Node test suite keep working
- add SunCalc vendor global to globals.d.ts; WeatherObject/WeatherUtils/CalendarUtils
  remain cross-file script globals (declared by their own .ts)
- ignore compiled outputs (incl. defaultmodules/weather/providers/*.js) in git + eslint

Gate: build clean, eslint/prettier clean, 357 unit tests pass (the 1 failure is the
pre-existing macOS-only systeminformation test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e 3)

Migrate the 8 server-side helper files via parallel translation, behavior unchanged:
- calendar/{node_helper,calendarfetcher}, newsfeed/{node_helper,newsfeedfetcher},
  weather/node_helper, updatenotification/{node_helper,git_helper,update_helper} -> src/*.ts
- node_helpers use `import NodeHelper = require("node_helper")` so the upgraded
  ambient node_helper module (ThisType<NodeHelperInstance> on create()) types
  `this.sendSocketNotification`/`this.name`/custom fields inside the definition
- dynamic provider/module require() paths kept as runtime function-call requires
- require()/export = elsewhere; typed fields, strict fixes
- ignore the 8 compiled outputs in git + eslint

Gate: build/eslint/prettier clean, 357 unit tests pass (1 pre-existing macOS-only failure).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the global-script browser leaves to .ts, kept as TS *script* files (no
top-level import/export) so tsc emits global-defining JS 1:1. Runtime unchanged.
- js/{class,logger,defaults,animateCSS,socketclient,translator},
  translations/translations, defaultmodules/defaultmodules -> src/*.ts
- browser tsconfig sets alwaysStrict:false so no "use strict" is injected — js/class.js
  relies on sloppy mode (this===window, arguments.callee). Type-checking stays strict.
- browser-shims.d.ts: module/process/require shims for the dual-world files
  (logger/defaults are <script>-loaded AND require()-d in Node)
- globals.d.ts: drop globals now defined by their own migrated source
  (cloneObject/Translator/MMSocket/AnimateCSSIn/Out/defaultModules); add Class/io/mmPort
- class.ts keeps the John Resig pattern verbatim (arguments/.apply/xyz probe) with a
  scoped eslint-disable; constructor fns get `this: any`
- ignore the 8 compiled outputs in git + eslint

Gate: build/eslint/prettier clean, 357 unit tests pass incl. the JSDOM specs that
load compiled js/class.js and js/translator.js (validates global-script emit).
The 1 failure is the pre-existing macOS-only systeminformation test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the two browser-core files, kept as TS script files (no top-level
import/export) so tsc emits global-defining JS 1:1. Runtime unchanged.
- js/module.ts: the Module base + Module.register/create/definitions contract that
  forever-JS community modules depend on. Typed `const Module: ModuleConstructor =
  Class.extend(...)` so all consumers (loader, default modules) get a register()
  whose definition object is ThisType<any> (this.config/this.translate/... resolve).
  configMerge keeps its variadic `arguments` (scoped eslint-disable); new MMSocket cast.
- js/loader.ts: runtime <script>/<link> injection loader (the dynamic module loader).
  DOM element vars typed (HTMLScriptElement/HTMLLinkElement); explicit default return.
- globals.d.ts: drop Module/Loader ambient consts (now defined by their own source);
  give Class.extend + Module.register ThisType<any>; add Log.debug.
- ignore compiled js/module.js + js/loader.js in git + eslint

Gate: build/eslint/prettier clean, 357 unit tests pass incl. the JSDOM module specs
(cmp_versions, module_spec) that load compiled js/module.js. 1 pre-existing macOS failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tage 6)

Migrate the 8 default-module entry scripts + alert's notificationFx helper via
parallel translation, kept as TS script files (no import/export) so each still
calls window.Module.register(...) on load exactly as before. Runtime unchanged.
- alert/{alert,notificationFx}, calendar/calendar, clock/clock,
  compliments/compliments, helloworld/helloworld, newsfeed/newsfeed,
  updatenotification/updatenotification, weather/weather -> src/*.ts
- loosen ModuleProperties to its index signature (modules override base methods with
  varying signatures); register() keeps ThisType<any> so this.config/translate resolve
- add NotificationFx + Cron ambient globals
- strict fixes preserving behavior: DOM coercions (colSpan number, opacity String()),
  Date arithmetic via getTime(), (window as any) theme hooks, (Object as any).groupBy

Gate: build/eslint/prettier clean, 357 unit tests pass (incl. compliments/weather/
calendar default-module specs). 1 pre-existing macOS-only failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the final 15 source files; all 63 source files are now TypeScript.
- browser: js/main (window MM orchestrator), js/vendor -> src/*.ts (script files)
- server: js/{app,server,electron,node_helper,releasenotes,ip_access_control,
  check_config,systeminformation,alias-resolver}, clientonly/index,
  serveronly/{index,watcher}, defaultmodules/calendar/debug -> src/*.ts
- alias-resolver keeps the Module._resolveFilename monkey-patch (private members
  accessed via the untyped require result); side-effect server files use `export {}`
  so they are modules (avoids global-scope collisions between script files)
- node_helper base: `this: any` on the Class.extend definition methods
- main: `const MM: any`, instanceof (Module as any), config -> `declare var` (reassigned),
  Log.setLogLevel added to LogType, optional sendNotification params
- dynamic require() of community modules + hardcoded "js/electron.js" spawn args kept
- globals.d.ts: drop MM/vendor (now self-defined), add modulePositions; collapse the
  per-file compiled-output ignore lists into directory globs (all sources live in src/)

Gate: build/eslint/prettier clean, 357 unit tests pass. 1 pre-existing macOS failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All 63 source files are TypeScript, compiled with `strict: true` and `allowJs: false`
(zero .js remaining in src/). Final polish:
- add `prepack` build hook so `npm pack` ships compiled JS (+ source maps)
- add src/README.md documenting the src->output layout, the two-project (browser/server)
  no-bundler build, the editing rules (no top-level import/export in browser globals),
  and an upstream-merge playbook for this fork

Strictness: the canonical `strict` bundle is fully enabled and enforced. The extra
opt-in flags noUncheckedIndexedAccess and exactOptionalPropertyTypes are intentionally
deferred (~54 mostly-mechanical guards, low safety gain over the current any-tolerant
cross-module boundaries) — documented as future tightening.

Verification: build + eslint + prettier + markdownlint + cspell clean; 357 unit tests
pass; 291 server-side e2e assertions pass (server boots from the compiled TS entry
points). Remaining failures are environmental only: the macOS-only systeminformation
test (asserts a Linux platform) and the browser-render e2e specs (Playwright chromium
not installed in this environment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tighten beyond the strict bundle. The ~54 resulting errors were all unchecked
indexed/match access (arr[i], regexMatch[n], split()[n], Object.keys()[0]) now
typed `T | undefined`; fixed with non-null assertions where surrounding guards
(.length checks, known match shapes, bounded loops) already guarantee presence —
no runtime behavior changed.

Verification: build clean under the tightened flags; eslint + prettier clean;
357 unit tests pass; full e2e suite 461/461 pass (Playwright chromium installed,
so the browser-render specs now run against the compiled TypeScript). Only the
macOS-only systeminformation unit test still fails (asserts a Linux platform).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dule.ts

js/module.ts is now a proper `class Module` instead of `Class.extend({...})`. Base
methods/fields are class members with a real `this` type (no more ThisType<any> hack
for the base), and `instanceof Module` now works natively.

The runtime module contract is preserved exactly:
- Module.register / Module.definitions / Module.create unchanged in behavior.
- Module.create still clones the registered definition (cloneObject) and builds an
  instance via the dynamic Module.extend(definition).
- Module.extend now returns `class Subclass extends Module` with the definition's
  members applied to the subclass prototype, and still wraps any method that calls
  `this._super()` so it invokes the overridden base method — the same inheritance
  contract community modules (and the default newsfeed module) rely on.
- `defaults`/`requiresVersion` stay prototype-level (assigned on Module.prototype) so
  a module definition can shadow them; per-instance state uses class fields. The base
  constructor runs the overridable init().
- Module.register keeps `ModuleProperties & ThisType<any>` so default-module
  definitions' `this` still type-checks.

class.js (Class/cloneObject) is retained — still used by Module.create (cloneObject)
and by js/node_helper.ts.

Verification: build clean under strict + noUncheckedIndexedAccess + exactOptionalProperty
Types; eslint + prettier clean; 357 unit tests pass (incl. module create/register,
cmp_versions, and newsfeed `_super`); full e2e 461/461 pass (all modules render in a
real browser).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…de_helper.ts

js/node_helper.ts is now a proper `class NodeHelper` instead of `Class.extend({...})`
— typed methods/fields with a real `this`, and no `require("./class")`.

Runtime contract preserved exactly:
- NodeHelper.create(definition) still returns a CONSTRUCTOR (NodeHelper.extend(def)),
  which js/app.js instantiates with `new` per module — unchanged from before.
- NodeHelper.extend returns `class Subclass extends NodeHelper` with the definition's
  members applied to the subclass prototype, still wrapping any `this._super()` method
  so it calls the overridden base — the inheritance contract community node helpers rely on.
- The base constructor runs the overridable init(); checkFetchStatus/checkFetchError stay
  as statics. name/path/expressApp/io are runtime-assigned (declared, not instance fields).
- The `node_helper` alias ambient type (ThisType for default node_helper definitions'
  `this`) is unchanged; default node_helpers still type-check.

class.js is no longer required by node_helper (only its cloneObject remains in use, by
Module.create). Verification: build clean under the tightened strict flags; eslint +
prettier clean; 357 unit tests pass; e2e 461/461 pass (server loads every default
node_helper and runs the socket lifecycle).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… system

Module and NodeHelper are now real TypeScript classes, so the John Resig
"Simple JavaScript Inheritance" Class system in js/class.ts is dead code. Removed it;
js/class.ts now contains only the cloneObject helper (still used by Module.create to
deep-clone a module definition, and loaded as a browser global via index.html).
- drop the now-unused `Class` ambient global from globals.d.ts
- update the globals.d.ts cross-file note and src/README.md (the class.js sloppy-mode
  rationale for alwaysStrict:false is gone; alwaysStrict stays off to preserve the
  historical sloppy-mode loading of browser scripts)

class_spec.js already only exercised cloneObject (incl. the lockStrings logging path),
so it stays green. Verification: build clean under strict + noUncheckedIndexedAccess +
exactOptionalPropertyTypes; eslint + prettier + cspell clean; 357 unit tests pass;
e2e 461/461 pass (translations_spec loads class.js for cloneObject; modules render).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert top-level CommonJS require() to ES import syntax throughout the server-side
TypeScript files, giving real cross-module types (the migration previously left these
as any-typed require()). Runtime behavior preserved.

- node builtins / typed npm packages / internal modules -> `import` (default, named,
  or `import * as` as appropriate). export=-style internals use a default import;
  named-export internals (#server_functions, ../provider-utils) use named imports.
- ip_access_control.ts switched from `export = { ipAccessControl }` to a named
  `export function ipAccessControl`, so consumers import it by name.
- side-effect `require("./alias-resolver")` -> `import "./alias-resolver"` (kept first
  so the Module._resolveFilename patch runs before any aliased import resolves).
- alias-resolver.ts: node:path/node:module via import (Module cast to any for the
  private _resolveFilename/_mmAliasPatched members).
- add src/types/untyped-modules.d.ts ambient stubs for packages without types
  (express, suncalc, feedme, html-to-text) so their imports type-check under strict.
- eslint: ignore the internal alias specifiers (logger, node_helper) in
  import-x/no-unresolved (resolved at runtime by alias-resolver, typed via aliases.d.ts).

Deliberately left as runtime require() (cannot/should not be static imports):
- dynamic paths (community node_helpers, weather providers, defaultmodules list,
  ${root}/js/deprecated, check_config's utils, package.json version, watcher configs);
- dual-world browser scripts with no static TS export (./defaults, ./logger, ./vendor);
- ../package.json (rootDir/outDir layout mismatch); lazy in-function requires (pm2,
  clientonly's electron/child_process); logger.ts's node:util (browser script).
Also surfaced/fixed a stale defaultmodules/calendar/debug.ts (dev-only tester) against
the current CalendarFetcher API (8-arg constructor, fetchCalendar()).

Verification: build clean under strict + noUncheckedIndexedAccess +
exactOptionalPropertyTypes; eslint + prettier clean; 357 unit tests pass; e2e 461/461
pass (server boots, alias-resolver-first ordering + import interop verified, all
node_helpers load and modules render).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ules

Reduce the TS lint ignore list from 3 blanket rule-disables to 1.
- @typescript-eslint/no-require-imports: off -> error. Converted the remaining
  `import X = require(...)` forms (4 node_helpers, utils' logger) to default imports;
  the ~20 genuine require() calls that must stay (dynamic runtime paths, dual-world
  browser scripts with no static TS export, lazy/optional deps) now each carry a
  justified inline `// eslint-disable-next-line ... -- <reason>` instead of a blanket off.
- @typescript-eslint/no-empty-function: off -> error with allow:["methods","arrowFunctions"]
  (no-op override points / default callbacks); the few plain empty function expressions
  got an explicit `{ /* no-op */ }` body.
- @typescript-eslint/no-explicit-any stays off, now documented: any is confined to
  dynamic external boundaries (config, API payloads, DOM). Reducing it is a separate
  per-subsystem domain-typing effort.

New accidental require()/empty functions are now caught by lint. Verification: build
clean (strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes); eslint +
prettier clean; 357 unit tests pass; e2e 461/461 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a shared ambient MMConfig (+ ModuleConfigEntry) interface in
src/types/config.d.ts (global type for both the browser and server projects) and
type the global `config` / `global.config` / `window.config` as MMConfig instead of
`any`. Well-known fields (address, port, language, cors, modules, logLevel, ...) are
typed from js/defaults.js; an index signature keeps the object extensible (users and
modules add arbitrary keys), and genuinely dynamic fields (httpHeaders, electronOptions,
userAgent) stay any — so this is a safe, no-new-behavior refinement of the prior any.

Every config.* / global.config.* access across the codebase is now type-checked; this
already caught a latent issue (app.ts passed config.modules[].position, string|undefined,
to Utils.moduleHasValidPosition(string) — the param is now widened to match the runtime
guard that already handled undefined).

Verification: build clean (strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes);
eslint + prettier clean; 357 unit tests pass; e2e 461/461 pass. Types-only change, no
runtime emit difference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ig / WeatherError)

Add shared ambient weather types in src/types/weather.d.ts (WeatherDataPoint,
WeatherConfig, WeatherError, WeatherDataCallback, WeatherErrorCallback) and apply them
across the weather subsystem to replace `any`:
- 11 providers: fetcher (HTTPFetcher | null), onDataCallback/onErrorCallback (typed
  callbacks), setCallbacks/constructor params, and the #generate* data-builder methods
  now return WeatherDataPoint / WeatherDataPoint[]; config typed WeatherConfig on 8 of 11.
- weather/node_helper.ts: initWeatherProvider(config: WeatherConfig); provider data/error
  callbacks typed WeatherDataPoint[] / WeatherError.
- weather.ts (browser module): incoming socket weather arrays typed WeatherDataPoint[],
  createWeatherObject(data: WeatherDataPoint); weatherobject.ts feelsLike(): number.

Raw external-API payloads (parsedData/response/JSON, #convertWeatherType, .map callbacks)
intentionally stay `any`. 3 providers (openmeteo/smhi/ukmetofficedatahub) keep
`config: any` where optional-field access (config.lon.toFixed, config.type list checks,
validateCoordinates(config)) made WeatherConfig noisy — escape hatch, no behavior change.

Verification: build clean (strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes);
eslint + prettier clean; 357 unit tests pass (provider specs); e2e 461/461 pass (weather
module renders). Types-only change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add shared ambient calendar types in src/types/calendar.d.ts (CalendarEvent,
CalendarConfig) and apply them to replace `any`:
- calendarfetcher.ts: events: CalendarEvent[]; httpFetcher: HTTPFetcher.
- calendarfetcherutils.ts: filterEvents(data, config: CalendarConfig): CalendarEvent[];
  newEvents: CalendarEvent[] (the pushed event object is a CalendarEvent).
- calendar.ts (browser module): event iterations (map/filter/sort/forEach/groupBy),
  createEventList(): CalendarEvent[], single-event helper params, and broadcastEvents
  all typed CalendarEvent; one `as CalendarEvent[]` at the raw socket-payload boundary.

CalendarEvent fields are optional with an index signature; startDate/endDate stay `any`
(treated as both ms-strings and numbers). Raw ICS/moment/DOM/socket-payload values stay
`any`. Two escape hatches: shouldEventBeExcluded keeps `config: any` (optional
excludedEvents under noUncheckedIndexedAccess), and event.title (optional) uses
`as string` at RegExp.test sites to preserve the existing coercion behavior.

Verification: build clean (strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes);
eslint + prettier clean; 357 unit tests pass (calendar specs); e2e 461/461 pass (calendar
renders). Types-only change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add shared ambient newsfeed types in src/types/newsfeed.d.ts (NewsItem, NewsfeedConfig)
and apply them to replace `any`:
- newsfeedfetcher.ts: items: NewsItem[]; httpFetcher: HTTPFetcher (the parsed item
  object pushed in parser.on("item", ...) is a NewsItem).
- newsfeed/node_helper.ts: createFetcher(feed, config: NewsfeedConfig);
  broadcastFeeds' accumulator typed Record<string, NewsItem[]>.
- newsfeed.ts (browser module): item iterations (map/sort/forEach/findIndex),
  newsItems/updatedItems: NewsItem[], getUrlPrefix(item: NewsItem); one
  `feeds[feed] as NewsItem[]` cast at the raw socket-payload boundary.

NewsItem fields are optional with an index signature; pubdate/publishDate stay `any`
(feed-dependent). Raw feed-parser values, socket payloads, moment/DOM stay `any`.
Two callbacks in generateFeed keep `item: any` (they call .toLowerCase()/.slice() on the
optional title/description directly; typing would need runtime guards = behavior change).

Verification: build clean (strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes);
eslint + prettier clean; 357 unit tests pass; e2e 461/461 pass (newsfeed renders).
Types-only change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er/module)

Reduce `any` in the DOM core by typing module instances as the real Module class and
DOM nodes as DOM types (no runtime change):
- main.ts: modules: Module[]; modulesStarted/createDomObjects/updateDom/
  updateDomWithContent/moduleNeedsUpdate/updateModuleContent/hideModule/showModule take
  `module: Module`; sendNotification sender/sendTo: Module | null (runtime passes null);
  module[m] undefined-guard from noUncheckedIndexedAccess. DOM elements kept as
  HTMLElement (existing casts/guards). MM stays `any` (orchestrator literal); animation
  names / payload / options / the selection-method internals stay `any` (escape hatch:
  they use .withClass/.exceptModule not on the Module class).
- loader.ts: moduleObjects: Module[]; Module.create result Module | undefined;
  bootstrapModule mObj: Module (module-data info object stays any). thisModule.hide() is
  called arg-less -> `(thisModule as any).hide()` rather than loosen the hide() signature.
- module.ts: hasAnimateIn/hasAnimateOut widened boolean -> `string | false` (they hold
  animation names at runtime); other instance state stays any.

Verification: build clean (strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes);
eslint + prettier clean; 357 unit tests pass (incl. JSDOM module specs); e2e 461/461 pass.
Types-only change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dules

Give the alert module's NotificationFx a real type instead of `any`:
- globals.d.ts: NotificationFxOptions + NotificationFxInstance interfaces; declare
  NotificationFx as `new (options?: NotificationFxOptions) => NotificationFxInstance`.
- alert.ts: alerts map typed `{ [key]: NotificationFxInstance }`; dropped the two
  `new (NotificationFx as any)(...)` casts — `new NotificationFx({...})` now type-checks
  its options and the `.show()`/`.dismiss()` calls.
- notificationFx.ts: constructor `options: NotificationFxOptions`.
- helloworld.ts: getTemplateData(): object.

The other default modules need no change: clock.ts is already any-free (tsc infers the
createElement DOM types), and the remaining `any` in compliments/updatenotification is at
genuine dynamic boundaries (notification payloads, Cron timestamp, git-diff status) where
`any` is appropriate.

Verification: build clean (strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes);
eslint + prettier clean; 357 unit tests pass; e2e 461/461 pass. Types-only change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ire declaration

The browser-shims `require` ambient (declared for the dual-world logger.js's
require("node:util")) was `(id: string) => any`. In the combined editor view (root
tsconfig loads both browser-shims and @types/node) it shadowed NodeRequire, so
serveronly/watcher.ts's `require.cache[require.resolve(configPath)]` showed TS2339
"Property 'cache'/'resolve' does not exist" — an editor-only false error (the server
project build, which excludes browser-shims, was always clean).

Merge `cache` and `resolve` onto the shim via `declare namespace require`, so the
require.cache/require.resolve usage type-checks wherever the shim is the effective
`require` type (browser project + editor) while the real NodeRequire still applies in
the server build.

Verification: both project builds clean; root/editor tsconfig no longer reports the
errors; eslint clean. Types-only .d.ts change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No genuinely-dead exports/functions/globals remained after the migration. The only
"unused" code was required-but-unused parameters (Express req, electron handler args,
base notificationReceived payload, etc.). Prefixed them with `_` and enabled
noUnusedLocals + noUnusedParameters in tsconfig.base.json so future dead locals/params
are caught at compile time.

Also folds in working-tree tidy-ups: serveronly/index app.start config typed MMConfig
(dropped the now-redundant `export {}` — the `import app` already makes it a module),
socketclient basePath check simplified, browser-shims unused eslint-disable removed.
Restored the `@typescript-eslint/no-explicit-any: off` line in the TS eslint block that
had been dropped from the working tree (it produced 670 errors at the intentional
dynamic-`any` boundaries); reducing those is a separate domain-typing effort.

Verification: build clean (strict + noUnusedLocals/Parameters + noUncheckedIndexedAccess
+ exactOptionalPropertyTypes); eslint + prettier clean; 357 unit pass; e2e 461/461 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reorganize the TypeScript sources from output-mirroring folders
(src/js, src/defaultmodules) into concern-based folders, so a feature's
browser script and its server node_helper live together:

  src/js/        -> src/server/ (Node core) + src/client/ (browser core)
  src/defaultmodules/ -> src/modules/<feature>/

Each concern now compiles via its own per-concern tsc project that pins a
single rootDir -> outDir mapping and the correct lib/types, replacing the
former browser/server split. The seven projects (server, client,
modules.server, modules.client, serveronly, clientonly, translations) are
built with `tsc -b`; output is byte-for-output-set identical (63 files into
js/, defaultmodules/, serveronly/, clientonly/, translations/). Runtime
filenames are unchanged (they are contractual: index.html <script>,
package.json main, dynamic require).

Cross-project seams that can no longer be statically imported (a project
may not emit a .ts owned by another rootDir) are bridged with ambient
declarations instead:
  - browser globals defined in one project, used in another
    (Module, defaultModules, translations) -> consumer-only shims
    src/types/{module,client}-consumer.d.ts
  - #-subpath modules produced by src/server but consumed by src/modules /
    src/serveronly (#http_fetcher, #app, #alias-resolver, #server_functions)
    -> ambient `declare module`s in src/types/aliases.d.ts
Server-internal uses of #server_functions switched to a relative
./server_functions import so they keep real types.

The root tsconfig.json stays a noEmit editor/LSP convenience config (one
combined program) and excludes the consumer shims (their globals are
provided by the real defining sources there).

src/README.md documents the new layout and the cross-project rules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the toolchain from npm to Yarn 4.14.1, pinned via the
`packageManager` field and managed by Corepack.

- Add `.yarnrc.yml` with `nodeLinker: node-modules` (keeps the flat
  node_modules layout electron, pm2 and playwright expect; no PnP).
- Replace package-lock.json with yarn.lock; .gitignore now ignores
  package-lock.json / pnpm-lock.yaml and the Yarn runtime artifacts
  (.yarn/* except patches/plugins/sdks, .pnp.*), and tracks yarn.lock.
- Yarn Berry does not run npm-style user `pre*` lifecycle scripts (and
  neither does `node --run`), so the implicit "build before X" guarantee
  the pretest/prestart/preserver/preconfig:check hooks provided is now
  inlined as `node --run build && ...` into config:check, server, start,
  start:dev and the test:* scripts. The lifecycle events Yarn does honor
  (postinstall, prepare, prepack) are kept.
- install-mm -> `yarn workspaces focus --production` (Yarn-native prod
  install); install-mm:dev -> `yarn install && yarn playwright install
  chromium && node --run build`.
- Add jsonc-eslint-parser to devDependencies: it is a peer dependency of
  eslint-plugin-package-json that npm auto-installed but Yarn does not.

CI (.github/workflows): drop the `cache: "npm"` hints, add a
`corepack enable` step after setup-node (so the pinned Yarn is on PATH
across node-version swaps), and switch npm/npx invocations to yarn
(playwright install, @electron/rebuild, serialport, electron-rebuild).

Docs: CONTRIBUTING, the bug-report issue template and the npm-registry
publish steps in Collaboration.md updated to Yarn (`yarn npm publish`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add CLAUDE.md: repo guidance covering the src/->runtime compile-in-place
  model, the hard runtime invariants (no top-level import/export in browser
  script files; runtime assets live in defaultmodules/ not src/; community
  modules stay JS; contractual output filenames), the Yarn 4 gotchas (no
  pre* hooks -> build inlined; peers not auto-installed), the command set,
  the verify-before-done gate, and known pre-existing test/spell failures.
- README.md: add a "This fork: TypeScript + Yarn" section with the dev
  quick-start (corepack enable; node --run install-mm:dev / test / server).
- src/README.md: fix the now-stale build section that claimed npm pre*
  hooks run the build — they don't under Yarn; build is inlined into the
  main scripts. Use yarn / node --run commands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sdetweil
Copy link
Copy Markdown
Collaborator

sdetweil commented Jun 4, 2026

What do we do about the 1200 other modules?

how does typescript support arrays of different/unknown types. Mixed types

@KristjanESPERANTO
Copy link
Copy Markdown
Collaborator

This would be a huge change. Please elaborate why you think we should migrate to typescript and yarn.

I use both in other projects, but see more down then up sides for this project at the moment.

@pierre-jochem
Copy link
Copy Markdown

What do we do about the 1200 other modules?

how does typescript support arrays of different/unknown types. Mixed types

Good point and see this more as a draft. I will invest more time into the modules approach.

@pierre-jochem
Copy link
Copy Markdown

This would be a huge change. Please elaborate why you think we should migrate to typescript and yarn.

I use both in other projects, but see more down then up sides for this project at the moment.

Yarn is not necessary, but with a proper typing in place I see more advantages. Just need to address a proper interface for the modules or at least the existing ones are still compatible.

@pierrejochem pierrejochem marked this pull request as draft June 5, 2026 05:26
@pierrejochem
Copy link
Copy Markdown
Author

What do we do about the 1200 other modules?

how does typescript support arrays of different/unknown types. Mixed types

  1. I tried 3 modules this morning and all of these were still compatible.

  2. Where do we have this situation? I will invest time to find a solution

@KristjanESPERANTO
Copy link
Copy Markdown
Collaborator

KristjanESPERANTO commented Jun 5, 2026

Before you invest more work in this, I suggest we first discuss the overall direction and constraints for a potential TypeScript migration.

Simplicity is a core project principle for MagicMirror² (see our manifesto). So my main concern is that a mandatory build step and a full TypeScript migration would significantly increase day-to-day complexity. If the main goal is type safety, I do not think we need to migrate the source to TypeScript right away.

A possible middle ground could be:

  • Keep the runtime source in JavaScript.
  • Introduce TypeScript-based type checking for JavaScript as a separate no-emit validation step.
  • Evaluate the result incrementally before considering a broader migration.

That would improve type safety while keeping the contributor experience approachable.

For a migration of this size, it would have been better to open an issue or a minimal PR first and seek alignment on direction and constraints before investing in the implementation. In its current size, this PR is too large to review effectively, but I think we can use it now to catch up on that discussion.

One more process question: we currently do not have a formal AI policy, but it looks like AI was heavily involved in this PR. Was this PR mostly AI-generated? Also, adding a CLAUDE.md file should likely be discussed separately.

Thanks for the effort and initiative.

Note: I'm only one of the core maintainers, maybe others have different perspectives on this.

@KristjanESPERANTO
Copy link
Copy Markdown
Collaborator

Ah, and you need to direct the PR to the develop branch, not to master.

@pierrejochem
Copy link
Copy Markdown
Author

pierrejochem commented Jun 5, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants