refactor(renderer): type sidebar + top-level SFCs β drop @ts-nocheck (TS migration follow-up 4/6)#4252
Conversation
- Drop `// @ts-nocheck` from `editor.ts` (1,737 lines) and
`preferences.ts` (197 lines). Both remain Options Stores; Pinia's
Options-Store typing is fully inferrable from a typed `state: () =>
State` factory, so converting to Setup Store would churn ~80 call sites
for no expressive gain.
- Consolidate `IFileState` (in `@shared/types/files.ts`) and
`IDocumentState` (in `store/help.ts`) into a single canonical shape;
`IDocumentState` becomes an alias re-exported from `help.ts`.
- Replace the `currentFile: {}` empty-object sentinel with
`IFileState | null`; the two `hasKeys(this.currentFile)` sites become
explicit null checks, and `?? {}` fallbacks become `?? null`.
- Restore the missing `RENAME_IF_NEEDED` action that `project.ts` was
calling through an `(editorStore as any)` cast (legacy action dropped
in the VuexβPinia port). Cast + eslint-disable removed.
- Tighten the IPC contract entries that mismatched the runtime
payloads: `mt::rename`, `mt::response-file-move-to`,
`mt::window-tab-closed`, `mt::close-window-confirm`,
`mt::export-success`, `mt::response-print`, `mt::update-format-menu`;
add `mt::response-file-save-as`. `SaveOptions.encoding` now accepts
the runtime `FileEncoding` object as well as the legacy string form.
- Trim the "Deferred work" item for editor.ts and update the Pinia
stores section in `docs/dev/TYPESCRIPT.md` to reflect that the
remaining Options Stores are typed.
Verified: `pnpm typecheck` clean; `pnpm test:unit` 550/550 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop @ts-nocheck from the sideBar component group (9 SFCs) and the top-level Main.vue / pages (app.vue, preference.vue). - Add src/renderer/src/components/sideBar/types.ts with shared TreeNode, TreeFileNode, TreeFolderNode, SearchResult/SearchMatch and TabDescriptor shapes β drawn from treeCtrl + ripgrep bridge runtime payloads. - Type defineProps via the generic form for every sideBar SFC. - Type element refs (HTMLInputElement / HTMLDivElement) where mouse/key handlers need them. - Narrow window.marktext.initialState in global.d.ts so app/preference pages can read theme/codeFontFamily without casts. addStyles in app.vue now coalesces nullable URL params against DEFAULT_STYLE. - Refactor search.vue to hold a typed CancellableSearch handle instead of trying to read `.cancel` off a chained Promise<void>. - Fix unresolved bare-directory imports (`@/components/foo`) in app.vue and preference.vue by spelling out `/index.vue` (vue-tsc, unlike Vite, doesn't auto-resolve directories with an index.vue). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR is part of the renderer TypeScript migration follow-ups: it removes // @ts-nocheck from the sidebar and top-level page SFCs by adding explicit prop/types, and introduces shared sidebar type definitions plus a broader window.marktext global type.
Changes:
- Removed
// @ts-nocheckacross sidebar components and top-level page shells; added explicit prop typings and safer null-handling in templates/handlers. - Introduced shared sidebar domain types (
Tree*, search result shapes,TabDescriptor) to reduce ad-hocany/casts across SFCs. - Expanded
window.marktexttyping to includeinitialStateandpathsto support typed bootstrap data access.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types/global.d.ts | Extends window.marktext typing for bootstrap initialState and paths. |
| src/renderer/src/pages/preference.vue | Drops @ts-nocheck, uses explicit .vue imports, and hardens initial theme style application. |
| src/renderer/src/pages/app.vue | Drops @ts-nocheck, strengthens types around timers/props, and coalesces nullable bootstrap style values. |
| src/renderer/src/Main.vue | Removes @ts-nocheck (empty script setup). |
| src/renderer/src/components/sideBar/types.ts | Adds shared TS types for sidebar tree/search/tab surfaces. |
| src/renderer/src/components/sideBar/treeOpenedTab.vue | Adds typed props (TabDescriptor) and null-safe current file comparisons. |
| src/renderer/src/components/sideBar/treeFolder.vue | Adds typed props/refs and explicit return types for handlers. |
| src/renderer/src/components/sideBar/treeFile.vue | Adds typed props/refs and null-safe current file comparisons. |
| src/renderer/src/components/sideBar/tree.vue | Adds typed props and a typed accessor for createCache.dirname to remove template casts. |
| src/renderer/src/components/sideBar/toc.vue | Drops @ts-nocheck and types the TOC click handler payload. |
| src/renderer/src/components/sideBar/searchResultItem.vue | Drops @ts-nocheck and types computed values/handlers for search result rendering. |
| src/renderer/src/components/sideBar/search.vue | Drops @ts-nocheck, types search state, and refactors cancellable ripgrep usage. |
| src/renderer/src/components/sideBar/index.vue | Drops @ts-nocheck, types refs/width computations, and guards missing drag bar element. |
| src/renderer/src/components/sideBar/icon.vue | Drops @ts-nocheck and makes computed icon classes null-safe. |
Comments suppressed due to low confidence (1)
src/renderer/src/components/sideBar/search.vue:264
- The parameter type
executeSearch: boolean | unknown = truecollapses tounknown(sinceunknownabsorbs unions), which defeats the intent of typing and makesif (executeSearch)effectively accept any value. Prefer a plainbooleanparameter (and, if callers may pass non-boolean, coerce/validate inside) so the functionβs contract is clear and type-safe.
const handleFindInFolder = (executeSearch: boolean | unknown = true): void => {
nextTick(() => {
if (searchEl.value) {
searchEl.value.focus()
// `searchMatches.value` may carry a `selectedText` populated elsewhere
// (legacy contract from CodeMirror / find-in-page). Narrow defensively.
const selectedText = (searchMatches.value as { selectedText?: string } | undefined)
?.selectedText
if (selectedText) {
keyword.value = selectedText
if (executeSearch) {
search()
}
}
π‘ Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const handleClick = ({ slug }) => { | ||
| bus.emit('scroll-to-header', slug) | ||
| const handleClick = (data: { slug?: unknown }): void => { | ||
| bus.emit('scroll-to-header', data.slug) |
There was a problem hiding this comment.
Fixed in acf88e9b. Now narrows to a non-empty string and bails out early:
if (typeof data.slug !== 'string' || data.slug.length === 0) return
bus.emit('scroll-to-header', data.slug)| tabs: Array | ||
| }) | ||
| const props = defineProps<{ | ||
| projectTree: TreeNode |
There was a problem hiding this comment.
Fixed in acf88e9b β typed as TreeNode | null with a comment noting that the project store seeds it as null until a folder is opened, and the template's v-if="projectTree" guard now matches the prop type.
- toc.vue: narrow `data.slug` to a non-empty string before emitting
`scroll-to-header`. editor.vue builds a CSS selector with `#${slug}`, so
forwarding `unknown`/`undefined` could produce `#undefined` selectors and
silently no-op the scroll.
- tree.vue: type `projectTree` as `TreeNode | null` to match the runtime
shape (the project store seeds it as `null` until a folder is opened) and
the template's existing `v-if="projectTree"` guard. Non-nullable type made
the script section unsound and would hide future null-handling bugs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # docs/dev/TYPESCRIPT.md # src/renderer/src/store/editor.ts # src/shared/types/files.ts # src/shared/types/ipc.ts
β¦S migration follow-up 7/7) (#4255) * refactor(test): port specs to strict TS (drop @ts-nocheck, ESM imports) Convert all 17 e2e specs and the e2e helpers module from CommonJS `require()` to ESM `import`, drop `// @ts-nocheck` from every test file, and add real types for the Playwright/Electron handles (ElectronApplication, Page) plus the helper return shapes. Vitest specs gain explicit `import { describe, it, expect, ... } from 'vitest'` so they no longer rely on the implicit globals when type-checked. Also adds the `main_renderer/*` path alias (already in vitest.config.ts) to tsconfig.base.json so the native-theme unit spec resolves against src/main from TS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(renderer): type prefComponents schemas and leaf SFC controls Removes // @ts-nocheck from the preference-component schema modules and the leaf input controls (bool, compound, fontTextBox, range, select, textBox, titlebar), plus a few related TS files (window-controls, exportSettings/exportOptions, sideBar/help, image uploader services, KeybindingConfigurator). Adds prefComponents/common/types.ts with shared PrefControlBaseProps / PrefControlProps<T> / PrefSelectOption<T> helpers; per-control props extend the base with their specific value-prop name and onChange signature. Static option arrays in each */config.ts now return PrefSelectOption<T>[]; the static `themes` array in theme/config.ts gains a ThemeDescriptor type; KeybindingConfigurator exposes a typed UiKeybinding shape and Map<string, string> fields. sideBar/config.ts adds a local Window.__VUE_I18N__ shape so the debug / language-polling helpers no longer need a blanket nocheck. * refactor(stores): type editor + preferences Pinia stores - Drop `// @ts-nocheck` from `editor.ts` (1,737 lines) and `preferences.ts` (197 lines). Both remain Options Stores; Pinia's Options-Store typing is fully inferrable from a typed `state: () => State` factory, so converting to Setup Store would churn ~80 call sites for no expressive gain. - Consolidate `IFileState` (in `@shared/types/files.ts`) and `IDocumentState` (in `store/help.ts`) into a single canonical shape; `IDocumentState` becomes an alias re-exported from `help.ts`. - Replace the `currentFile: {}` empty-object sentinel with `IFileState | null`; the two `hasKeys(this.currentFile)` sites become explicit null checks, and `?? {}` fallbacks become `?? null`. - Restore the missing `RENAME_IF_NEEDED` action that `project.ts` was calling through an `(editorStore as any)` cast (legacy action dropped in the VuexβPinia port). Cast + eslint-disable removed. - Tighten the IPC contract entries that mismatched the runtime payloads: `mt::rename`, `mt::response-file-move-to`, `mt::window-tab-closed`, `mt::close-window-confirm`, `mt::export-success`, `mt::response-print`, `mt::update-format-menu`; add `mt::response-file-save-as`. `SaveOptions.encoding` now accepts the runtime `FileEncoding` object as well as the legacy string form. - Trim the "Deferred work" item for editor.ts and update the Pinia stores section in `docs/dev/TYPESCRIPT.md` to reflect that the remaining Options Stores are typed. Verified: `pnpm typecheck` clean; `pnpm test:unit` 550/550 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(renderer): type sidebar + top-level SFCs (drop @ts-nocheck) Drop @ts-nocheck from the sideBar component group (9 SFCs) and the top-level Main.vue / pages (app.vue, preference.vue). - Add src/renderer/src/components/sideBar/types.ts with shared TreeNode, TreeFileNode, TreeFolderNode, SearchResult/SearchMatch and TabDescriptor shapes β drawn from treeCtrl + ripgrep bridge runtime payloads. - Type defineProps via the generic form for every sideBar SFC. - Type element refs (HTMLInputElement / HTMLDivElement) where mouse/key handlers need them. - Narrow window.marktext.initialState in global.d.ts so app/preference pages can read theme/codeFontFamily without casts. addStyles in app.vue now coalesces nullable URL params against DEFAULT_STYLE. - Refactor search.vue to hold a typed CancellableSearch handle instead of trying to read `.cancel` off a chained Promise<void>. - Fix unresolved bare-directory imports (`@/components/foo`) in app.vue and preference.vue by spelling out `/index.vue` (vue-tsc, unlike Vite, doesn't auto-resolve directories with an index.vue). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(renderer): type preference page SFCs (drop @ts-nocheck) Removes the `// @ts-nocheck` suppression from the 12 preference window page-level SFCs (the leaf controls under `prefComponents/common/` were already typed in PR-B) and resolves the type errors that surface as a result. - General/Editor/Markdown/Theme/Image/Spellchecker/Sidebar pages now type the `onSelectChange(type, value)` mutator against `PreferencesState`, narrow the typed `storeToRefs` output, and use the shared `PrefSelectOption<T>` helper for inline option arrays. - The image uploader page types its picgo detection state, awaits the Promise-returning `commandExists.exists` (which the original code invoked synchronously under `@ts-nocheck`), and narrows `UploaderServiceId` lookups. - The keybindings page types the `KeybindingConfigurator` ref and the raw IPC return shapes (`mt::keybinding-get-keyboard-info` and `mt::keybinding-get-pref-keybindings` are still `ret: unknown` in the IPC contract; the cast is local). - The key-input dialog now uses `defineProps<T>()` generics, types its `useTemplateRef` mount as `HTMLInputElement`, and types the KeyboardEvent handlers. No production behavior changes. Verified via `pnpm typecheck` and `pnpm test:unit` (550 tests pass). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(renderer): type editor + components SFCs (drop @ts-nocheck) Remove `// @ts-nocheck` from all editorWithTabs/* and components/* SFCs covered by PR-D.2: editor.vue, index.vue, tabs.vue, notifications.vue, sourceCode.vue, about, commandPalette, exportSettings, import, loading, recent, rename, search, titleBar. - Convert defineProps to TS generic form; type bus listeners, DOM refs, and Element Plus instance refs (loose where Element Plus does not ship adequate types). - Use PR-A's typed editor store: `IFileState`, `currentFile?.id` guards. - Keep Muya/CodeMirror surfaces as local `any` aliases pinned near the top of each file; replace when upstream TS Muya lands. - Fix lurking bug: rename dialog called `editorStore.rename(...)` which has always been `RENAME(...)` in the store. - Fix lurking bug: sourceCode handleImageAction fallback called `setCursorAtFirstLine()` with no argument; passes `editor.value` now to mirror the other call site. - Add a minimal `underscore` shim (debounce only) to src/types/shims.d.ts β no @types/underscore on npm and we only consume `debounce`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(merge): align cursor prop type with IFileState.cursor: unknown After merging PR-A (stores) with PR-D.2 (editor SFCs), the cursor prop in editorWithTabs/index.vue and editor.vue still declared `object`/`object | null` even though the typed editor store now returns `unknown` for cursor. Widen the prop type to `unknown` at both boundaries β the prop is forwarded verbatim into muya without runtime introspection, so unknown is the honest shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(any): replace `any` with `unknown`/proper types in 4 hot spots - src/main/ipc/fs.ts: cast through `string | NodeJS.ArrayBufferView` instead of `any` when forwarding to fs-extra writeFile/outputFile. - src/renderer/src/contextMenu/tabs/index.ts: MenuItemShape uses `(...args: unknown[]) => void` and `[key: string]: unknown`. - src/renderer/src/store/commandCenter.ts: reuse the existing `CommandDescriptor` from src/renderer/src/commands instead of a duplicate `Command` shape; tighten `normalizeAccelerator` return to `string[]` (both branches always returned string[]). - src/shared/types/typedEmitter.ts: replace the `any[]` fallback in the conditional listener-args type with `unknown[]`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(eslint): tighten @typescript-eslint/no-explicit-any to error - src/types/muya.d.ts: file-level disable with explanatory comment β this ambient bridge to legacy JS muya is intentionally `any` until upstream TS muya (https://github.com/marktext/muya) replaces it. - src/main/filesystem/watcher.ts: chokidar's `ignored` callback options bag defies the bundled `WatchOptions` type; targeted line disable with a `--` justification, plus an explicit comment. - eslint.config.js: flip the rule from `warn` to `error` for the .ts scope. - docs/dev/TYPESCRIPT.md: rewrite the "Deferred work" section β every item landed across PRs #4249β#4255. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(stores): address Copilot review feedback on PR #4249 - BootstrapEditorConfig.markdownList: MarkdownDocument[] β string[]. Main process actually sends raw markdown strings from `_markdownToOpen` (see src/main/windows/editor.ts:162). Drop the `md as unknown as string` cast in the renderer bootstrap path; pass `md` through directly. - mt::close-window-confirm: tighten the IPC contract from `[unsavedFiles?: unknown[]]` to `[unsavedFiles: UnsavedFile[]]` (required, not optional β main iterates the array and crashes on undefined). - Lift UnsavedFile to src/shared/types/files.ts so renderer and main share one source of truth. Internal writeMarkdownFile call sites cast through `Parameters<typeof writeMarkdownFile>[2]` instead of the previous `type SaveOptions = any` shim. - RENAME_IF_NEEDED now updates `window.DIRNAME` when the renamed tab is the active currentFile, so dirname-based link resolution doesn't keep using the old folder until the user switches tabs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ipc): tighten mt::keybinding-save-user-keybindings ret to boolean The main-process handler (src/main/app/index.ts:828) returns the boolean from `setUserKeybindings()` (src/main/keyboard/shortcutHandler.ts:123), but the IPC contract typed `ret` as `void`. KeybindingConfigurator.save() was double-casting through `as unknown as boolean` to compensate. Drop the cast and let the contract speak for itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): use Date#getDate() (day-of-month) in helpers.ts temp filename `getDateAsFilename()` was using `Date#getDay()` (0β6, day of week) instead of `Date#getDate()` (1β31, day of month). The generated temp-directory prefix didn't actually reflect the calendar date and could collide across different days that fall on the same weekday β exacerbated by short random suffixes and parallel Playwright workers. Pre-existing bug from before the TS port (test/e2e/helpers.js); flagged by Copilot review on PR #4251. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sideBar): address Copilot review feedback on PR #4252 - toc.vue: narrow `data.slug` to a non-empty string before emitting `scroll-to-header`. editor.vue builds a CSS selector with `#${slug}`, so forwarding `unknown`/`undefined` could produce `#undefined` selectors and silently no-op the scroll. - tree.vue: type `projectTree` as `TreeNode | null` to match the runtime shape (the project store seeds it as `null` until a folder is opened) and the template's existing `v-if="projectTree"` guard. Non-nullable type made the script section unsound and would hide future null-handling bugs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(editor): address Copilot review feedback on PR #4253 - editor.vue `imageAction` folder branch: pass `currentPathname` instead of `null as unknown as string`. `moveImageToFolder` calls `path.dirname(pathname)` whenever the image is a string (e.g. paste / drag / image-selector), which would crash on `dirname(null)`. Using `currentPathname` resolves relative paths against the active file's dir, matching the sibling call sites. - sourceCode.vue `onMounted`: reset `scrollTop` to `0` rather than `undefined as unknown as number`. `IFileState.scrollTop: number` is non-optional; comparisons elsewhere (`> currentFile.scrollTop`) would otherwise see `undefined` and behave oddly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(uploader): persist agreedToLegalNotices on the cached services singleton `getServices()` builds a new object each call, so mutating `getServices()[id].agreedToLegalNotices = true` only touched a throwaway instance β the checkbox bound to the file-local `uploadServices` cache (line 384) never saw the update. Mutate `uploadServices` directly so the agreed state survives. Flagged by Copilot review on PR #4254. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Part 4/6 of the TypeScript migration deferred-work cleanup. Drops
// @ts-nocheckfrom the sidebar SFCs and the top-level page shells.Changes (13 files)
tree.vue,treeFile.vue,treeFolder.vue,treeOpenedTab.vue,icon.vue,index.vue,search.vue,searchResultItem.vue,toc.vue.Main.vue,pages/app.vue,pages/preference.vue.src/renderer/src/components/sideBar/types.ts:TreeFileNode,TreeFolderNode,TreeNodeβ narrow versions of the runtime shapes built bytreeCtrl.ts.SearchRange,SearchMatch,SearchResultβ ripgrep result shape consumed bysearch.vue/searchResultItem.vue.TabDescriptorβ alias ofIFileStateat the sideBar boundary.src/types/global.d.tsβ broadenedwindow.marktextto includeinitialStateandpaths.Notable adjustments
tree.vueβ added acreateCacheDirnamecomputed because the underlyingcreateCacheref union ({ dirname, type } | {}) doesn't exposedirnamedirectly.search.vueβ refactored to hold theCancellableSearchhandle separately from the chained.then().catch()(the chained promise becomesPromise<void>and loses.cancel).app.vueβ tightened theaddStylesboundary with explicit coalescing againstDEFAULT_STYLEbecause URL-parsedinitialStatecarries nullables.app.vue/preference.vueβ spelled out/index.vueon directory imports: vue-tsc's bundler resolver doesn't follow Vite's directory-with-index.vue convention without an explicit path.Test plan
pnpm typecheckβ cleanpnpm test:unitβ 550/550 passpnpm exec playwright test test/e2e/launch.spec.ts test/e2e/tabs.spec.tsβ 3/3 passπ€ Generated with Claude Code