.js → .ts Migration Progress — details
Per-package detail for the migration plan in ./js_to_ts_progress.md. Add a subsection per package when work begins, and write per-file entries immediately so a session interrupt loses at most one entry.
Format
## packages/<name>
Session date: YYYY-MM-DD
Branch: <branch-name>
Files processed:
- `relative/path/file.ts` — short note on notable changes (caller updates, type fixes, dead code removed, etc.). If trivial, just "converted; no caller updates" is enough.
Files skipped:
- `relative/path/file.js` — reason (vendored, config, blocked by <X>, etc.)
Verification:
- `yarn tsc --noEmit`: <result>
- `yarn test` / `cd packages/<name> && yarn jest <pattern>` (where applicable): <result>
- `yarn syncFuzzer start --steps 5` (for app-cli / lib sync-target work): <result>
Notes / `review-later.md` entries added:
- <file:line> — <one-liner about what was logged>
Conventions for "Files processed" entries
- Caller updates are part of the same commit as the conversion. Mention them in the entry only if there are several or they are non-obvious. A single
.defaultadd at the consumer is implicit. - Type fixes the conversion surfaces (e.g. a previously-
anyparameter that the cast reveals to be too narrow) are noted. If applied as a separate commit, mention the commit subject. - Dead code removed (unused private fields, unreachable branches) is noted with the original-line reference.
- Pre-existing smells flagged to
review-later.mdare noted (file path + one-line summary).
packages/app-cli
Session date: 2026-05-22
Files processed (under packages/app-cli/app/):
command-undone.ts— converted; siblingcommand-donekept asrequire().command-mknote.ts— typed localnote: NoteEntityso post-Note.savereassignment type-checks; existing fire-and-forgetNote.updateGeolocationmarkedvoid.command-mktodo.ts— same pattern ascommand-mknote.command-todo.ts— switchedBaseModel.TYPE_NOTEtoModelType.Noteto match the narrowerloadItemsparameter;toSavetypedNoteEntity.command-search.ts— converted;notebookCLI arg name forces anid-denylistdisable on the args type.command-status.ts— converted; no caller updates.command-server.ts— LoggeraddTarget('console', …)→TargetType.Consoleenum; partial console mock cast throughunknowntoConsole.command-tag.ts—loadItem(ModelType.Tag as ModelType.Note, …)to work around the narrow signature; entity typesNoteEntity/TagEntity.gui/NoteMetadataWidget.ts—export default; consumer inapp-gui.jsupdated to.default.gui/NoteListWidget.ts—export default+ consumer;itemRenderercallback typedNoteEntity.gui/ConsoleWidget.ts—export default+ consumer; private fields typed.gui/NoteWidget.ts—export default+ consumer;as stringcasts in thedoAsyncclosure preserve "read at execution time" semantics that thelastLoadedNoteId_guard relies on.ResourceServer.ts—export default+ consumer;LinkHandlertype for the handler callback;server-destroyaugmentation cast onserver_.destroy().help-utils.ts— destructuredrequire()shape preserved;'cli'string literals →AppType.Cli;eslint-disable import/prefer-default-exporton the single named export.renderMetadatamdparameter tightened toSettingItemin a follow-up commit. (Also a follow-up to untrack the leftover compiledhelp-utils.jsafter a staging restore left it tracked.)autocompletion.ts—CompletionResult = string | CompletionList(CompletionList = string[] & { prefix?: string });yargs-parserconsumed viaimport = require();voidon the existing fire-and-forgetthen(…).cli-utils.ts—CliUtilsinterface declared for the namespace object; redundantprintArrayempty-rows early return removed; the deadif (i >= a.length)branch inmakeCommandArgsremoved with a comment pointing atreview-later.md;promptsynthesises aWritable & { muted: boolean }intersection.app-gui.ts— biggest of the round; dropped deadinputMode_field + the two unusedINPUT_MODE_*static constants; typedprocessShortcutKeysexplicitly asboolean; definedKeymapIteminterface andNoteLinkdiscriminated union;voidon three pre-existing fire-and-forget calls. Single consumer inapp.ts:442updated to.default.
Follow-up tightening commits (separate from each file's main conversion):
Tighten BaseCommand.description() return type to string— base method throws but every subclass returns a string; tightened sohelp-utilsdoesn't need a cast.Tighten app-cli help-utils renderMetadata md parameter—any→SettingItem.Tighten app-gui Redux store/state any types—store_: any→Store<State>; four(state: any) => …mappers →(state: State) => ….Tighten app-gui App any type to Application— required addingexportto theApplicationclass inapp.ts; surfaced a real type mismatch (ResourceServer.setLogger(this.app().logger())was passingLoggerWrapperinto aLogger-typed setter — fixed by widening ResourceServer's logger types).Simplify ResourceServer logger types to just LoggerWrapper—Logger | LoggerWrappercollapses toLoggerWrappersinceLoggeris structurally assignable to it.Tighten remaining app-gui any types—noteLinks→ discriminated union;updateNoteText note: any→NoteEntity | null.Tighten cli-utils prompt mutableStdout any to a typed intersection— replaced twoanycasts withWritable & { muted: boolean }and a typedthison the write callback.Extract PromptOptions interface from StatusBarWidget.prompt— same anonymous{ cursorPosition?: number; secure?: boolean }shape was duplicated across three places; extracted asPromptOptionsexported fromStatusBarWidget.Final-review cleanup on app-cli conversions— mirrored theLoggerWrapperwidening toapp-gui.ts logger_/setLogger; reordered theNoteEntityimport to group with@joplin/lib/*; trimmed the 5-line closure-cast comment inNoteWidget.Trim cli-utils explanatory comments— removed theprintArrayempty-rows comment entirely; shortened theprompt_initialTextrationale.Use typed LinkSelector import in app-cli app-gui—LinkSelectorwas already a typed.tsclass; replaced therequire('./LinkSelector.js').default+anyfield with a typed import and dropped the stale eslint-disable. Caught during a final consistency-check pass over the branch.Trim block comments in app-cli autocompletion and cli-utils— same consistency-check pass: shortened theCompletionListrationale block to one line and removed the cli-utilsmakeCommandArgsdead-branch post-mortem (the dead branch is already gone; the note lives on inreview-later.md).
Files skipped:
app/main.js— shebang preservation issue (see "Files to never touch" in the plan).app/build-doc.js— dev-time documentation generator, low value.app/fuzzing.js— 2400+ lines, exploratory test runner.tests/feature_*.js,tests/HtmlToHtml.js,tests/support/createSyncTargetSnapshot.js— feature tests / test helpers; deferred to a follow-up round.tests/support/plugins/**/*.js— intentionally JS plugin fixtures.
Verification:
yarn tsc --noEmitfrom the repo root: clean after every commit.yarn syncFuzzer start --steps 5from the repo root: green at three checkpoints (after the first batch of commands, after the gui widget round, after the cli-utils / help-utils / autocompletion round).cd packages/app-cli && yarn jest command-done.test: 1 passing (smoke check aftercommand-undone).cd packages/lib && yarn jest synchronizer.basics.test: 24/24 (baseline before the round; not re-run after every commit).
Notes / review-later entries added during this round (entries record generic locations; an on-disk review-later.md is per-environment and not committed):
packages/app-cli/app/app.ts loadItem/loadItems— the typed parameter (Note | Folder | 'folderOrNote') is narrower than the runtime support, which falls through toBaseItem.itemClass(type).loadByTitlefor other types includingModelType.Tag.command-tagcasts at the call site to compensate.packages/app-cli/app/autocompletion.ts:103— in the'item'argument completion branch,notes.map(n => n.title)is spread butfolders.map(n => n.title)is not — folder titles end up pushed into the completion list as a single nested array element. Likely-bug; theas unknown as stringcast in the converted file preserves the behaviour.packages/app-cli/app/cli-utils.ts makeCommandArgs—if (i >= a.length)whereais{ required, name }is unreachable (a.lengthisundefined). The else branch always ran; preserved by removing the dead conditional. Likely intendedargs['_'].length.
packages/renderer
Session date: 2026-05-25Branch: claude/chore/renderer--js-to-ts
Files processed (under packages/renderer/):
stringUtils.ts—module.exports = { surroundKeywords }→ named export;require('html-entities').AllHtmlEntitiesswapped for the typedimport { AllHtmlEntities } from 'html-entities'; newKeyword = string | RegexKeyword | StringKeyworddiscriminated union typed off the in-code comment that documented the keyword shapes. cSpell ignore block added around the diacritic-replacement table. Existingrequire('../../stringUtils.js')consumers inhighlight_keywords.tsunchanged (named exports remain reachable from the namespace).urlUtils.ts—module.exports = urlUtilsnamespace object → named exports (urlDecode,isResourceUrl,parseResourceUrl,ParsedResourceUrl). Internal self-references switched fromurlUtils.X(...)to bare calls. Existingrequire('../urlUtils.js')consumers inlinkReplacement.tsandlink_open.tsunchanged.defaultNoteStyle.ts—module.exports = {...}→export default {...}. Single consumer inMdToHtml.ts:76updated to a top-of-fileimport defaultNoteStyle from './defaultNoteStyle';.Tools/buildAssets.ts— dev-time script. Three function signatures typed (dirname(path: string),copyFile(source: string, dest: string),main());fs-extrakept asconst fs = require('fs-extra')(implicitanyfromrequireper project convention; renderer has no@types/fs-extraand adding the dep is out of scope for a mechanical conversion). Verified by runningyarn buildAssetsfrompackages/renderer/— outputs inassets/{abc,mermaid,katex,highlight.js}matched the prior structure.
Files skipped:
MdToHtml/rules/abc_render.jsandMdToHtml/rules/mermaid_render.js— shipped raw to browsers byTools/buildAssets.js(lines 49/52fs.copythe source.jstoassets/{mermaid,abc}/). Matches the same hazard pattern documented forpackages/lib/markJsUtils.jsin the plan's "Files to never touch" section: TS-emit CommonJS wrappers (Object.defineProperty(exports, '__esModule', …)) break in a raw<script>context.mermaid_render.jsadditionally assigns toEvent.target(a readonly DOM property in lib.dom.d.ts), so strict TS would require a cast. Skip until the build pipeline is updated (e.g. compile these via a webpack/esbuild step that emits a browser-friendly IIFE, or changebuildAssets.jsto copy the compiled output instead of the source).MdToHtml/rules/katex_mhchem.js— 1700+ lines vendored from the KaTeX repo (https://github.com/KaTeX/KaTeX/blob/master/contrib/mhchem/mhchem.js); already preceded by/* eslint-disable */. Falls under the plan's "Vendored / forked third-party code — preserved verbatim" rule.lib/renderer.js— 7-line// TODOstub with no callers found in repo. Flag for removal as dead code in a separate PR; not in scope for a mechanical conversion.tests/test-utils.js— empty file (0 bytes), no callers. Same: candidate for deletion, not conversion.jest.config.js— config file kept as.jsby design (plan "Files to never touch").
Verification:
yarn tsc --noEmitfrom the repo root: zero new errors in@joplin/renderer. (Pre-existing errors in@joplin/app-desktopWhiteboardEditor/*and@joplin/app-mobileservices/e2ee/crypto.tswere present on the branch base and are unrelated.)cd packages/renderer && yarn jest: 35/35 passing (5 suites).cd packages/app-cli && yarn test MdToHtml: 13/13 passing (cross-package renderer integration tests).cd packages/renderer && yarn buildAssets: ran clean; regenerated assets underassets/{abc,mermaid,katex,highlight.js}matched the prior structure. Note: regenerating overwrote a committed-staleassets/abc/abcjs-basic-min.js(v6.5.2 on disk vs v6.6.2 inpackage.json); restored before commit and noted as a pre-existing concern.
Notes / review-later entries added (generic locations; the on-disk review-later.md is per-environment and not committed):
packages/renderer/stringUtils.ts surroundKeywords—valueRegexfromRegexKeywordentries is passed verbatim intonew RegExp(...); onlypregQuote-escaped string keywords are safe. A pathological pattern ((a+)+$) compiledgiand run against note text causes catastrophic backtracking on every render. Source ofvalueRegexis search input — primarily self-inflicted but persists into saved searches.packages/renderer/assets/abc/abcjs-basic-min.js— committed v6.5.2 disagrees withpackage.json'sabcjs: 6.6.2; runningyarn buildAssetsregenerates and produces a dirty working tree.packages/renderer/lib/renderer.js— 7-line// TODOstub, no callers in repo; deletion candidate.packages/renderer/tests/test-utils.js— 0-byte file, no callers in repo; deletion candidate.
packages/lib
Session date: 2026-05-26Branch: claude/chore/lib--js-to-ts
Source files processed (under packages/lib/):
envFromArgs.ts— signature tightened to(args: string[] | null | undefined) => 'dev' | 'prod'.parameters.ts— introducedEnv,AppCredentials,ParametersForEnvtypes;parameters_typedRecord<Env, ParametersForEnv>;Setting.value('env')cast toEnv.randomClipperPort.ts— addedClipperPortStateinterface and localEnvalias.SyncTargetMemory.ts—default export class extends BaseSyncTarget; callers updated to default-import.SyncTargetNextcloud.ts— siblingSyncTargetWebDAVkept asrequire()(still JS at this point);checkConfigtemporarily typedoptions: anywith an eslint-disable.SyncTargetDropbox.ts— removed unused privateapi_field (set in constructor, never read); fixed a spelling slip flagged by cSpell; callers updated to default-import.SyncTargetWebDAV.ts— added exportedWebDavFileApiOptionsinterface shared bycheckConfigandnewFileApi_; siblingfile-api-driver-webdavkept asrequire().resourceUtils.ts— split previousmodule.exports = { ... }object into individual named exports; both destructured-requireand namespace-style callers continue to work.import-enex-html-gen.ts— removed an unusedoptionsparameter fromenexXmlToHtml(forwarded toenexXmlToHtml_which never read it, and no caller passed it);string-to-stream/@joplin/fork-saxkept asrequire()(no types); sax event-handler node/stream-output values stayany.
Test files processed (production-code counterpart was already TS):
database.test.ts— converted; benefits from existing TS types intest-utilsandBaseModel.TaskQueue.test.ts— constructor now passed aname;pushids switched from numeric to string to matchTaskQueue.ts's typed signature (JS coerced the numbers at runtime).ArrayUtils.test.ts— dropped a second arg toexpect().toEqual()in themergeOverlappingIntervalscase (rejected by Jest typings; silently ignored at runtime).services/KvStore.test.ts— no notable type adjustments.eventManager.test.ts— simplified test states cast throughunknown as AppStateatappStateEmitcall sites; callbacks typed via theAppStateChangeCallbackgeneric; dropped the now-redundant'use strict'.urlUtils.test.ts— added explicit row types for theparseResourceUrl/extractResourceUrlstest-case arrays; switched toimport * as urlUtils from './urlUtils'to preserve namespace-style call sites.mimeUtils.test.ts,pathUtils.test.ts,timeUtils.test.ts— no notable type adjustments.services/KeymapService.test.ts— negative-test data for "required properties missing" cast throughunknown as KeymapItem[]so the type checker accepts the deliberately malformed entries; pre-existingaccelerator: nulldiscrepancy left as-is (masked by rootstrict: false).import-enex-html-gen.test.ts— importsResourceEntityand types fixtures explicitly; oneattachment-imagefixture has awidthfield not onResourceEntity, castas ResourceEntitylocally (field is unread by the converter; removing it would alter long-standing data).models/Note.customSortOrder.test.ts— converted and renamed fromNote_CustomSortOrder.test.js(anddescribetitle updated) to match theFoo.aspect.test.tsconvention;Note.insertNotesAtcalls now pass all 5 args (false, falseforuncompletedTodosOnTop/showCompletedTodos, preserving prior falsy-undefined behaviour);is_todo: trueswitched tois_todo: 1to match the typed numeric schema;originalTimestampstyped asRecord<string, {…}>.
Dead code removed (separate commit):
parseUri.js— deleted; no callers in repo.
Follow-up tightening commits:
Type SyncTargetNextcloud.checkConfig with WebDavFileApiOptions— replacedoptions: anywith theWebDavFileApiOptionsimport fromSyncTargetWebDAV, dropping the eslint-disable. Done as a separate commit because the type wasn't exported untilSyncTargetWebDAVwas itself converted.Simplify eventManager.test by dropping redundant generics— removed explicit<string>and<{ name: string }>args onappStateOn/appStateOff; the callback'sevent.valuealready lets TS infer the generic.
Files attempted but reverted:
markJsUtils.ts— converted in5de93c318and reverted in6e2a1b8ec. The TS-emit CommonJS wrappers (Object.defineProperty(exports, '__esModule', …)) broke the desktop note viewer at runtime (yarn test-ui markdownEditorfailed).yarn tsc --noEmitandyarn jestdid not catch it. Same hazard pattern as the renderer'sabc_render.js/mermaid_render.js: the file is shipped to a browser context where the CJS wrapper isn't valid. Documented in the plan's "Files to never touch" section; don't retry without first identifying the bundling path.
Files not yet processed (deferred to a follow-up round):
- Remaining
packages/lib/source.jsfiles not in this batch (models, services, sync helpers, etc.) — convert per the order in the plan's per-package strategy. markJsUtils.js— see above; blocked on the bundler/runtime fix.
Verification:
yarn tsc --noEmitfrom the repo root: clean after each commit (and after the merge withupstream/dev).cd packages/lib && yarn jest <suite>against the touched test files: green per-conversion.yarn syncFuzzer start --steps 5from the repo root: green after eachSyncTarget*conversion (primary verification path for sync-target files;yarn jestdoes not meaningfully exercise them).
Notes / review-later entries added during this round (generic locations; on-disk review-later.md is per-environment and not committed):
packages/lib/markJsUtils.js— see "Files attempted but reverted" above; the CJS-wrapper-in-browser hazard applies to any other file consumed by the desktop note viewer via a raw<script>-style include.packages/lib/SyncTargetDropbox.tsconstructor — removed unusedapi_field that was assigned but never read; verify no out-of-tree consumer (mobile/desktop overrides) relied on the field name before the next release.packages/lib/import-enex-html-gen.tsenexXmlToHtml— dropped unusedoptionsparameter; if a future plugin or external caller passes options it will now silently be ignored at compile time (the parameter was already a no-op at runtime).packages/lib/services/KeymapService.test.ts— pre-existingaccelerator: nullin test fixtures disagrees withKeymapItem(only present at rootstrict: false); flag for cleanup whenstrictis tightened.
Merge with upstream/dev (2026-05-26)
upstream/dev landed two overlapping PRs while this branch was in progress: #15523 (app-cli conversions) and #15532 (renderer conversions). Both equivalently converted files we had also converted locally. Merge resolution:
packages/renderer/stringUtils.ts— true content conflict (different stylistic choices: arrow-function vs function-declaration; module-scope vs function-scopediacriticReplacements; discriminated-union vs separate-interfaces forKeyword). Resolved by accepting upstream's version verbatim, since it was already reviewed and merged in#15532. Local commit19893150fbecomes redundant content-wise (the rename is preserved by the merge).- All app-cli
.js → .tsrenames and the rendererdefaultNoteStyle.ts/urlUtils.tsconversions resolved without conflict (identical content on both sides; git's rename detection collapsed them). .eslintignore/.gitignore— merge cleanly absorbed upstream's# AUTO-GENERATEDadditions for app-cli; re-ranyarn updateIgnoredpost-merge as a sanity check (no further diff).yarn tsc --noEmitpost-merge: clean.