Skip to content

refactor(superdoc): migrate SuperDoc.js to TypeScript#3479

Merged
caio-pizzol merged 8 commits into
mainfrom
caio-pizzol/SD-typecheck-superdoc-ts
May 25, 2026
Merged

refactor(superdoc): migrate SuperDoc.js to TypeScript#3479
caio-pizzol merged 8 commits into
mainfrom
caio-pizzol/SD-typecheck-superdoc-ts

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

SuperDoc's main public class is now TypeScript. The runtime is unchanged; the migration moves load-bearing contracts to TS-native syntax so future refactors get real class-shape feedback. Six commits: the migration itself, then five cleanup commits surfaced by review (dead JSDoc removal, dead cast removal, duplicate-import consolidation, lint-warning fixes, source-comment refresh).

What changed

  • packages/superdoc/src/core/SuperDoc.{js,ts} (2,379 lines). Real import type { ... } for the load-bearing types (alphabetized, deduplicated). config: InternalConfig = { ... } annotation defeats the literal-shape narrowing TS infers from the default object (the dominant error class on rename, ~140 of the initial 241). 15 declare class fields for late-init runtime state (activeEditor, toolbar, isCollaborative, socket, etc.) so TS knows the shape without emitting an own-property initializer - matches the previous runtime exactly. The five internal handles (superdocStore, commentsStore, highContrastModeStore, commentsList, app) are private declare so they stay off the public TS surface (consumer-typecheck fixture asserts this). ~60 method/parameter signatures annotated using existing JSDoc as the source of truth. Explicit return types added on state, getPresentationEditorForDocument, navigateTo, scrollToElement, and openSurface so deep-type-audit-supported-root sees no new any leaks. SuperDocEventMap converted from JSDoc typedef to TS interface and applied as EventEmitter<SuperDocEventMap> so unknown event names are a compile error.

  • Cleanup commits: removed the dead JSDoc typedef/event-map blocks now shadowed by TS interfaces/imports, dropped the /** @type {InternalConfig} */ this.config JSDoc-style casts (the field is already typed, the prefix was just stray text TS ignores), removed standalone /** @type {X} */ lines preceding declarations whose TS type is now explicit or trivially inferred. Some JSDoc tags remain where they still serve documentation purposes (public API docstrings, @deprecated markers, @example blocks, multi-line prose); they don't duplicate type information that TS now carries.

  • packages/superdoc/src/core/types/index.ts: added three runtime-only members to RuntimeDocument (rulers?, restoreComments?, removeComments?). RuntimeDocument is not re-exported by the public facade; public facade output verified byte-identical to main.

  • packages/superdoc/src/public/index.ts: refreshed the // Source: ./core/SuperDoc.js comment to point at the .ts source. The .js import specifier is intentional for ESM output and resolves to the .ts source during TypeScript builds.

What did NOT change

  • No as any shortcuts. The one any adapter (asEventListener) preserves the pre-existing JSDoc behavior and is wrapped in a scoped eslint-disable @typescript-eslint/no-explicit-any block with a documenting comment explaining why the looseness is correct (EventEmitter dispatches whatever each emit site supplies).
  • No new blanket suppressions. The two existing @ts-expect-error lines (__APP_VERSION__ const-replace + ?url asset import) are preserved.
  • No // @ts-check line in the .ts file.
  • packages/superdoc/dist/superdoc/src/public/index.d.ts and index.d.cts are byte-identical to main. Diff verified via md5sum against baseline captured from main before rename.

Verified

  • pnpm check:types β†’ PASS. 0 errors. (Baseline after rename was 241; reduced to 0 via the changes above, no shortcuts.)
  • pnpm check:public:superdoc β†’ PASS, 9 stages, 160.3s. Including:
    • consumer-typecheck-matrix (validates published .d.ts across bundler/node16/nodenext Γ— strict Γ— skipLibCheck).
    • deep-type-audit-supported-root (no new any regressions on supported root).
    • All four destructure-shape fixture tests (superdoc-stores-private, superdoc-events, whiteboard-data-shape, search-match) pass.
  • pnpm --filter superdoc test β†’ 1054/1054 tests pass.
  • pnpm --filter superdoc run lint β†’ 0 warnings on SuperDoc.ts.
  • Public-facing .d.ts files byte-identical to main. SuperDoc.d.ts (internal, relocated by ensure-types) changes shape (new EventMap interface block, TS-native type imports) but does not surface any on the supported root per the audit.

Review focus: the private declare choices on the five internal handles and the declare (no-initializer) choices on the 15 late-init runtime fields. Both follow the discipline of "TS shape only, no runtime change." If any of these should actually be public, flag the specific field. The SuperDocEventMap interface is a direct translation of the prior JSDoc typedef - payload references unchanged.

Renames the main SuperDoc public class to .ts and adopts TS-native
syntax for the surface (`declare` fields, real type imports,
explicit return types, EventMap generic).

What changed
- packages/superdoc/src/core/SuperDoc.{js,ts}: 2402-line rename.
  Added `import type { ... }` for every type previously declared as
  JSDoc `@typedef {import(...)}`. Replaced the `@type {Config}`
  on `this.config` with `config: InternalConfig` (the post-#init
  shape; defeats the literal-narrowing TS infers from the default
  object). Added 15 `declare` class fields for late-init runtime
  state (`activeEditor`, `toolbar`, `isCollaborative`,
  `socket`, etc.) so TS knows the shape without emitting an own
  property initializer - matches the previous runtime exactly.
  Marked the four internal handles (`superdocStore`,
  `commentsStore`, `highContrastModeStore`, `commentsList`,
  `app`) as `private declare` so the consumer-typecheck fixture
  still asserts they are not part of the public TS surface.
  Annotated ~60 method/parameter signatures (Editor, User,
  RuntimeDocument, string, etc.) using the existing JSDoc as the
  source of truth. Added explicit return types on `state`,
  `getPresentationEditorForDocument`, `navigateTo`,
  `scrollToElement`, and `openSurface` so deep-type-audit does
  not see new `any` leaks. Converted the JSDoc `SuperDocEventMap`
  typedef to a real TS `interface` and applied it as
  `EventEmitter<SuperDocEventMap>` so unknown event names are a
  compile error (consumer-typecheck fixture asserts this).
  Converted ~9 `/** @type {InternalConfig} */ (this.config)` JSDoc
  casts to plain `this.config` reads (no longer needed) and the
  remaining few JSDoc casts to TS `as` casts.

- packages/superdoc/src/core/types/index.ts: added three runtime-
  only members to `RuntimeDocument` (`rulers?`, `restoreComments?`,
  `removeComments?`). RuntimeDocument is internal-only (not re-
  exported from the public facade), so this does not change the
  consumer surface; it removes the need to cast at call sites.

What did not change
- No `as any` shortcuts. The one `any` adapter
  (`asEventListener`) preserves the pre-existing JSDoc behavior
  and carries an eslint-disable + documenting comment.
- No new blanket suppressions. The two existing `@ts-expect-error`
  lines (`__APP_VERSION__` const-replace + `?url` asset import)
  are preserved.
- No `// @ts-check` line in the .ts file.
- packages/superdoc/dist/superdoc/src/public/index.d.ts and
  index.d.cts are byte-identical to main.
- `packages/layout-engine/pm-adapter/src/*.d.ts` build artifacts
  are NOT included (intermittent leak from the build step).

Verified
- pnpm check:types -> PASS (0 errors; baseline after rename was 241).
- pnpm check:public:superdoc -> PASS, 9 stages, 158s.
  - deep-type-audit-supported-root PASS (no new `any` regressions
    on the supported root surface).
- pnpm --filter superdoc test -> 1054 / 1054 tests pass.
- Public-facing `dist/.../public/index.d.{ts,cts}` byte-identical
  to main. SuperDoc.d.ts (internal, relocated by ensure-types)
  changes shape (new EventMap interface, TS-native imports) but
  does not surface `any` on the supported root per the audit.
…fter migration

Pre-PR cleanup pass on the SuperDoc.ts migration. The TS-native
annotations from the prior commit made several JSDoc constructs
redundant; this strips them and refreshes stale prose. No runtime
change, no public surface change.

- SuperDoc.ts: removed the dead `@typedef {import(...)}` block (15
  declarations now shadowed by real `import type { ... }`), the
  large `@typedef {{ ... }} SuperDocEventMap` JSDoc and its 11
  payload `@typedef` siblings (replaced by the TS interface block
  added earlier), and `@param`/`@returns`/`@type` tags whose
  type info now duplicates the TS signature. Kept the prose comments
  that explain *why* (exception union shape, fonts-resolved
  listener-transport, runtime cast rationale for asEventListener).
  Updated the one in-body comment that still said "once SuperDoc.js
  is brought under the SD-2863 checkJs gate" - that's done.
- core/types/index.ts: three comment refreshes ("internal
  SuperDoc.js callsites" β†’ "internal SuperDoc callsites"). Import
  specifiers stay as `./SuperDoc.js` - they resolve to the .ts
  source via customConditions and renaming the extension would
  ripple to every importer.

Footprint: 2,586 β†’ 2,386 lines in SuperDoc.ts (-200), 128 β†’ 62 JSDoc
tags. Public-facing `dist/.../public/index.d.{ts,cts}` still
byte-identical to main.

Verified:
- pnpm check:types β†’ PASS
- pnpm check:public:superdoc β†’ PASS, 9 stages, 152.5s
- pnpm --filter superdoc test β†’ 1054 / 1054 tests pass
…ource comment

Second cleanup pass surfaced by review:

- SuperDoc.ts: removed the remaining JSDoc-style casts that became
  dead text in a .ts file. Two patterns:
  - `/** @type {InternalConfig} */ this.config` (8 occurrences) -
    the field is already `config: InternalConfig`, so the prefix
    was just stray text TS ignores.
  - `/** @type {X} */ (value)` inline local-var casts - TS infers
    from `value` directly.
  Also removed standalone `/** @type {X} */` lines preceding
  field/var declarations where TS inference matches the JSDoc
  intent. Kept JSDoc on public/static API where it carries prose.
- public/index.ts: refreshed the `// Source: ./core/SuperDoc.js`
  comment to point at the .ts source (the import specifier stays
  `.js` because it resolves through customConditions + the
  package exports map).

After this pass: SuperDoc.ts is 2,374 lines (was 2,386); remaining
JSDoc tags carry prose, not redundant types.

Verified:
- pnpm check:types β†’ PASS
- pnpm check:public:superdoc β†’ PASS, 9 stages, 160.3s
- pnpm --filter superdoc test β†’ 1054 / 1054 tests pass
- public dist .d.ts byte-identical to main
Lint surfaced two import-x/no-duplicates warnings (two import-type
blocks both from './types/index.js') and one no-unused-vars warning
(Document imported but only used in JSDoc, never as a TS annotation).

Merged the two ./types blocks, dropped Document, alphabetized
both type-import lists for stable diffs.
Both warnings are intentional patterns surfaced (not introduced) by
the migration. Adding scoped eslint-disable comments + explanations:

- asEventListener: widened the rule disable from
  next-line to enable/disable block so it covers the cast on the
  return line too. The any here is the documented runtime contract.
- syncCleanup placeholder: added a comment + eslint-disable-next-line
  for the intentional no-op that gets reassigned to the real
  cleanup once the sync observer registers.
The previous comment leaned on customConditions + the package exports
map. For this relative import, the actual rule is TS-side .js→.ts
specifier resolution. Reworded to say what is true.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ad8dc77cc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/superdoc/src/core/SuperDoc.ts Outdated
CI caught: the VS Code extension's contract test statically reads
the SuperDoc source via readFileSync to assert the public API
shape, and the path was hardcoded to SuperDoc.js. After the .js β†’ .ts
rename in this PR, the open() call fails with ENOENT.

Updated the path to SuperDoc.ts. Kept the read direct (no js β†’ ts
fallback): the source is now TS, period.

Verified: pnpm --filter superdoc-vscode-ext test β†’ 14 / 14 pass.
Codex review caught a real migration regression: SuperDoc.search was
annotated as `search(text: string)` during the .js β†’ .ts migration,
narrower than both the JSDoc on the line above (`@param {string |
RegExp}`) and the runtime contract (search.js:485-510 explicitly
branches on `isRegExp(patternInput)` and threads regex through to
`new RegExp(...)`).

Net effect on consumers: `superdoc.search(/foo/i)` was valid TS on
main and became a TS error after migration, even though the runtime
still accepts it.

Fix is one line: `search(text: string | RegExp)`. Added a fixture
regression guard in tests/consumer-typecheck/src/search-match.ts:
a runtime call site `sd.search(/hello/i)` plus an AssertEqual on
`Parameters<SuperDoc['search']>[0]` so the next migration can't
re-narrow without CI flagging it.

Verified: pnpm check:types β†’ PASS; pnpm check:public:superdoc β†’
PASS, 9 stages, 155s.
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.02649% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/superdoc/src/core/SuperDoc.ts 96.02% 5 Missing and 1 partial ⚠️

πŸ“’ Thoughts on this report? Let us know!

@caio-pizzol caio-pizzol merged commit 2869231 into main May 25, 2026
70 checks passed
@caio-pizzol caio-pizzol deleted the caio-pizzol/SD-typecheck-superdoc-ts branch May 25, 2026 00:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants