From c3353153adad16a7ad47cae904b87d17099dbb4b Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Mon, 4 May 2026 16:37:36 -0300 Subject: [PATCH 1/2] fix(types): gate public JSDoc helpers (SD-2833) --- packages/super-editor/package.json | 1 + .../trackChangesHelpers/addMarkStep.js | 26 ++- .../trackChangesHelpers/markDeletion.js | 29 ++- .../trackChangesHelpers/markInsertion.js | 20 +- packages/superdoc/scripts/check-jsdoc.cjs | 214 +++++++++++++++++- pnpm-lock.yaml | 3 + 6 files changed, 257 insertions(+), 36 deletions(-) diff --git a/packages/super-editor/package.json b/packages/super-editor/package.json index 6626fca0d1..d5ce53bfa6 100644 --- a/packages/super-editor/package.json +++ b/packages/super-editor/package.json @@ -175,6 +175,7 @@ "@types/mdast": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", + "@types/uuid": "catalog:", "@vitejs/plugin-vue": "catalog:", "@vue/test-utils": "catalog:", "canvas": "catalog:", diff --git a/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/addMarkStep.js b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/addMarkStep.js index a491d717c3..76893f16dd 100644 --- a/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/addMarkStep.js +++ b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/addMarkStep.js @@ -1,3 +1,4 @@ +// @ts-check import { TrackDeleteMarkName, TrackFormatMarkName, TrackedFormatMarkNames } from '../constants.js'; import { v4 as uuidv4 } from 'uuid'; import { TrackChangesBasePluginKey } from '../plugins/trackChangesBasePlugin.js'; @@ -18,11 +19,13 @@ import { getLiveInlineMarksInRange } from './getLiveInlineMarksInRange.js'; * @param {import('prosemirror-transform').AddMarkStep} options.step Step. * @param {import('prosemirror-state').Transaction} options.newTr New transaction. * @param {import('prosemirror-model').Node} options.doc Doc. - * @param {object} options.user User object ({ name, email }). + * @param {import('../../../core/types/EditorConfig.js').User} options.user User object ({ name, email }). * @param {string} options.date Date. */ export const addMarkStep = ({ state, step, newTr, doc, user, date }) => { + /** @type {{ formatMark?: import('prosemirror-model').Mark, step?: import('prosemirror-transform').AddMarkStep }} */ const meta = {}; + /** @type {string | null} */ let sharedWid = null; doc.nodesBetween(step.from, step.to, (node, pos) => { @@ -37,6 +40,7 @@ export const addMarkStep = ({ state, step, newTr, doc, user, date }) => { const rangeFrom = Math.max(step.from, pos); const rangeTo = Math.min(step.to, pos + node.nodeSize); + /** @type {import('prosemirror-model').Mark[]} */ const liveMarks = getLiveInlineMarksInRange({ doc: newTr.doc, from: rangeFrom, @@ -51,24 +55,28 @@ export const addMarkStep = ({ state, step, newTr, doc, user, date }) => { if (TrackedFormatMarkNames.includes(step.mark.type.name) && !hasMatchingMark(liveMarks, step.mark)) { const formatChangeMark = liveMarks.find((mark) => mark.type.name === TrackFormatMarkName); + /** @type {{ type?: string, attrs?: Record }[]} */ let after = []; + /** @type {{ type?: string, attrs?: Record }[]} */ let before = []; if (formatChangeMark) { - let foundBefore = formatChangeMark.attrs.before.find((mark) => - markSnapshotMatchesStepMark(mark, step.mark, true), + const beforeSnapshots = /** @type {{ type?: string, attrs?: Record }[]} */ ( + formatChangeMark.attrs.before || [] ); + const afterSnapshots = /** @type {{ type?: string, attrs?: Record }[]} */ ( + formatChangeMark.attrs.after || [] + ); + let foundBefore = beforeSnapshots.find((mark) => markSnapshotMatchesStepMark(mark, step.mark, true)); if (foundBefore) { - before = [ - ...formatChangeMark.attrs.before.filter((mark) => !markSnapshotMatchesStepMark(mark, step.mark, true)), - ]; + before = [...beforeSnapshots.filter((mark) => !markSnapshotMatchesStepMark(mark, step.mark, true))]; // The step restores the original mark for this type — remove the // corresponding "after" entry since the change has been reverted. - after = formatChangeMark.attrs.after.filter((mark) => getTypeName(mark) !== step.mark.type.name); + after = afterSnapshots.filter((mark) => getTypeName(mark) !== step.mark.type.name); } else { - before = [...formatChangeMark.attrs.before]; - after = upsertMarkSnapshotByType(formatChangeMark.attrs.after, { + before = [...beforeSnapshots]; + after = upsertMarkSnapshotByType(afterSnapshots, { type: step.mark.type.name, attrs: { ...step.mark.attrs }, }); diff --git a/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markDeletion.js b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markDeletion.js index a7526de216..3a42230e43 100644 --- a/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markDeletion.js +++ b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markDeletion.js @@ -1,3 +1,4 @@ +// @ts-check import { Mapping, ReplaceStep } from 'prosemirror-transform'; import { Slice } from 'prosemirror-model'; import { v4 as uuidv4 } from 'uuid'; @@ -10,14 +11,20 @@ import { findTrackedMarkBetween } from './findTrackedMarkBetween.js'; * @param {import('prosemirror-state').Transaction} options.tr Transaction. * @param {number} options.from From position. * @param {number} options.to To position. - * @param {object} options.user User object ({ name, email }). + * @param {import('../../../core/types/EditorConfig.js').User} options.user User object ({ name, email }). * @param {string} options.date Date. * @param {string} [options.id] Optional ID to use (for replace operations where insertion and deletion share the same ID). - * @returns {Object} Deletion map and deletionMark + * @returns {{ deletionMark: import('prosemirror-model').Mark, deletionMap: Mapping, nodes: import('prosemirror-model').Node[] }} Deletion map and deletion mark. */ export const markDeletion = ({ tr, from, to, user, date, id: providedId }) => { + /** + * @param {unknown} value + */ const normalizeEmail = (value) => (typeof value === 'string' ? value.trim().toLowerCase() : ''); const userEmail = normalizeEmail(user?.email); + /** + * @param {import('prosemirror-model').Mark | null | undefined} mark + */ const isOwnInsertion = (mark) => { const authorEmail = normalizeEmail(mark?.attrs?.authorEmail); // Word imports often omit authorEmail, treat missing as "own" to allow deletion. @@ -25,13 +32,16 @@ export const markDeletion = ({ tr, from, to, user, date, id: providedId }) => { return authorEmail === userEmail; }; - let trackedMark = findTrackedMarkBetween({ - tr, - from, - to, - markName: TrackDeleteMarkName, - attrs: { authorEmail: user.email || '' }, - }); + const trackedMark = + /** @type {{ from: number, to: number, mark: import('prosemirror-model').Mark } | null | undefined} */ ( + findTrackedMarkBetween({ + tr, + from, + to, + markName: TrackDeleteMarkName, + attrs: { authorEmail: user.email || '' }, + }) + ); let id; if (providedId) { @@ -59,6 +69,7 @@ export const markDeletion = ({ tr, from, to, user, date, id: providedId }) => { // - Own insertions are removed (collapsed). // - Existing deletions are reassigned to the new deletion mark ID. // - Non-deleted inline nodes are marked as deleted. + /** @type {import('prosemirror-model').Node[]} */ let nodes = []; tr.doc.nodesBetween(from, to, (node, pos) => { if (node.type.name.includes('table')) { diff --git a/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markInsertion.js b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markInsertion.js index 1438900f10..52bd72f0fa 100644 --- a/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markInsertion.js +++ b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markInsertion.js @@ -1,3 +1,4 @@ +// @ts-check import { v4 as uuidv4 } from 'uuid'; import { TrackInsertMarkName, TrackDeleteMarkName } from '../constants.js'; import { findTrackedMarkBetween } from './findTrackedMarkBetween.js'; @@ -8,7 +9,7 @@ import { findTrackedMarkBetween } from './findTrackedMarkBetween.js'; * @param {import('prosemirror-state').Transaction} options.tr Transaction. * @param {number} options.from From position. * @param {number} options.to To position. - * @param {object} options.user User object ({ name, email }). + * @param {import('../../../core/types/EditorConfig.js').User} options.user User object ({ name, email }). * @param {string} options.date Date. * @param {string} [options.id] Optional ID to use (for replace operations where insertion and deletion share the same ID). * @returns {import('prosemirror-model').Mark} Insertion mark. @@ -17,13 +18,16 @@ export const markInsertion = ({ tr, from, to, user, date, id: providedId }) => { tr.removeMark(from, to, tr.doc.type.schema.marks[TrackDeleteMarkName]); tr.removeMark(from, to, tr.doc.type.schema.marks[TrackInsertMarkName]); - let trackedMark = findTrackedMarkBetween({ - tr, - from, - to, - markName: TrackInsertMarkName, - attrs: { authorEmail: user.email || '' }, - }); + const trackedMark = + /** @type {{ from: number, to: number, mark: import('prosemirror-model').Mark } | null | undefined} */ ( + findTrackedMarkBetween({ + tr, + from, + to, + markName: TrackInsertMarkName, + attrs: { authorEmail: user.email || '' }, + }) + ); let id; if (providedId) { diff --git a/packages/superdoc/scripts/check-jsdoc.cjs b/packages/superdoc/scripts/check-jsdoc.cjs index 7644c02849..94646bb51b 100644 --- a/packages/superdoc/scripts/check-jsdoc.cjs +++ b/packages/superdoc/scripts/check-jsdoc.cjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * SD-2863: per-file checkJs gate for the public-contract surface. + * SD-2833: per-file checkJs gate for the public-contract surface. * * Why this exists in this shape (and not as a plain `tsc -p tsconfig.checkjs.json`): * @@ -24,8 +24,7 @@ * Adding a new file to the gate: * * 1. Add `// @ts-check` as the first line of the file. - * 2. Add the file's path (relative to `packages/superdoc/`) to - * CHECKED_FILES below. + * 2. Add the file's repo-relative path to CHECKED_FILES below. * 3. Run `node packages/superdoc/scripts/check-jsdoc.cjs` and fix what * surfaces. * @@ -37,19 +36,211 @@ const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); +const ts = require('typescript'); const CHECKED_FILES = [ - 'src/helpers/schema-introspection.js', - 'src/composables/use-find-replace.js', - 'src/composables/use-password-prompt.js', + 'packages/superdoc/src/helpers/schema-introspection.js', + 'packages/superdoc/src/composables/use-find-replace.js', + 'packages/superdoc/src/composables/use-password-prompt.js', + 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/addMarkStep.js', + 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markDeletion.js', + 'packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markInsertion.js', ]; +const PUBLIC_ENTRY_FILES = [ + 'packages/superdoc/src/index.js', + 'packages/superdoc/src/super-editor.js', + 'packages/superdoc/src/ui.js', +]; + +const REACHABILITY_EXEMPT_CHECKED_FILES = new Set([ + // These files predate SD-2833. They are kept under the gate because their + // typedefs feed exported SuperDoc configuration types, but they are reached + // through implementation imports rather than direct public barrel exports. + 'packages/superdoc/src/composables/use-find-replace.js', + 'packages/superdoc/src/composables/use-password-prompt.js', +]); + const packageDir = path.resolve(__dirname, '..'); const repoRoot = path.resolve(packageDir, '..', '..'); const tscBin = path.join(repoRoot, 'node_modules', '.bin', 'tsc'); const tsconfigPath = path.join(packageDir, 'tsconfig.json'); +const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.vue']; + +const PACKAGE_EXPORT_SOURCES = { + '@superdoc/super-editor': 'packages/super-editor/src/index.ts', + '@superdoc/super-editor/blank-docx': 'packages/super-editor/src/editors/v1/core/blank-docx.ts', + '@superdoc/super-editor/document-api-adapters': 'packages/super-editor/src/editors/v1/document-api-adapters/index.ts', + '@superdoc/super-editor/markdown': 'packages/super-editor/src/editors/v1/core/helpers/markdown/index.ts', + '@superdoc/super-editor/parts-runtime': 'packages/super-editor/src/editors/v1/core/parts/init-parts-runtime.ts', + '@superdoc/super-editor/ui': 'packages/super-editor/src/ui/index.ts', +}; + +const SOURCE_ALIASES = [ + ['@core/', 'packages/super-editor/src/editors/v1/core/'], + ['@extensions/', 'packages/super-editor/src/editors/v1/extensions/'], + ['@features/', 'packages/super-editor/src/editors/v1/features/'], + ['@components/', 'packages/super-editor/src/editors/v1/components/'], + ['@helpers/', 'packages/super-editor/src/editors/v1/core/helpers/'], + ['@converter/', 'packages/super-editor/src/editors/v1/core/super-converter/'], + ['@tests/', 'packages/super-editor/src/editors/v1/tests/'], + ['@translator', 'packages/super-editor/src/editors/v1/core/super-converter/v3/node-translator/'], + ['@utils/', 'packages/super-editor/src/editors/v1/utils/'], + ['@shared/', 'shared/'], +]; + +const toRepoRelative = (abs) => path.relative(repoRoot, abs).split(path.sep).join('/'); + +const tryResolveSourcePath = (basePath) => { + if (path.extname(basePath)) { + if (fs.existsSync(basePath) && fs.statSync(basePath).isFile()) return basePath; + if (basePath.endsWith('.js')) { + for (const extension of ['.ts', '.tsx']) { + const sourcePath = `${basePath.slice(0, -3)}${extension}`; + if (fs.existsSync(sourcePath)) return sourcePath; + } + } + return null; + } + + for (const extension of SOURCE_EXTENSIONS) { + const sourcePath = `${basePath}${extension}`; + if (fs.existsSync(sourcePath)) return sourcePath; + } + + for (const extension of SOURCE_EXTENSIONS) { + const sourcePath = path.join(basePath, `index${extension}`); + if (fs.existsSync(sourcePath)) return sourcePath; + } + + return null; +}; + +const resolveSourceSpecifier = (specifier, containingFile) => { + if (Object.prototype.hasOwnProperty.call(PACKAGE_EXPORT_SOURCES, specifier)) { + return path.join(repoRoot, PACKAGE_EXPORT_SOURCES[specifier]); + } + + if (specifier.startsWith('@superdoc/super-editor/')) { + return tryResolveSourcePath( + path.join(repoRoot, 'packages/super-editor/src', specifier.slice('@superdoc/super-editor/'.length)), + ); + } + + if (specifier.startsWith('.')) { + return tryResolveSourcePath(path.resolve(path.dirname(containingFile), specifier)); + } + + for (const [alias, target] of SOURCE_ALIASES) { + if (specifier === alias.replace(/\/$/, '')) return tryResolveSourcePath(path.join(repoRoot, target)); + if (specifier.startsWith(alias)) { + return tryResolveSourcePath(path.join(repoRoot, target, specifier.slice(alias.length))); + } + } + + return null; +}; + +const createSourceFile = (filePath) => { + const source = fs.readFileSync(filePath, 'utf8'); + const scriptKind = filePath.endsWith('.tsx') + ? ts.ScriptKind.TSX + : filePath.endsWith('.ts') + ? ts.ScriptKind.TS + : ts.ScriptKind.JS; + return ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, scriptKind); +}; + +const findExportReachableTargets = (filePath) => { + if (!/\.[jt]sx?$/.test(filePath)) return []; + + const sourceFile = createSourceFile(filePath); + const importedBindings = new Map(); + const reachableTargets = []; + + for (const statement of sourceFile.statements) { + if (!ts.isImportDeclaration(statement)) continue; + if (!statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) continue; + + const target = resolveSourceSpecifier(statement.moduleSpecifier.text, filePath); + const clause = statement.importClause; + if (!target || !clause) continue; + + if (clause.name) importedBindings.set(clause.name.text, target); + + const namedBindings = clause.namedBindings; + if (!namedBindings) continue; + if (ts.isNamespaceImport(namedBindings)) { + importedBindings.set(namedBindings.name.text, target); + continue; + } + if (ts.isNamedImports(namedBindings)) { + for (const element of namedBindings.elements) { + importedBindings.set(element.name.text, target); + } + } + } + + for (const statement of sourceFile.statements) { + if (!ts.isExportDeclaration(statement)) continue; + + if (statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) { + const target = resolveSourceSpecifier(statement.moduleSpecifier.text, filePath); + if (target) reachableTargets.push(target); + continue; + } + + if (!statement.exportClause || !ts.isNamedExports(statement.exportClause)) continue; + for (const element of statement.exportClause.elements) { + const localName = (element.propertyName || element.name).text; + const target = importedBindings.get(localName); + if (target) reachableTargets.push(target); + } + } + + return [...new Set(reachableTargets)]; +}; + +const collectPublicExportSurface = () => { + const seen = new Set(); + const stack = PUBLIC_ENTRY_FILES.map((file) => path.join(repoRoot, file)); + + while (stack.length > 0) { + const current = stack.pop(); + if (!current || seen.has(current)) continue; + + seen.add(current); + for (const target of findExportReachableTargets(current)) { + if (!seen.has(target)) stack.push(target); + } + } + + return seen; +}; + +const hasJSDocTypeSurface = (abs) => { + if (!abs.endsWith('.js')) return false; + const source = fs.readFileSync(abs, 'utf8'); + return /\/\*\*[\s\S]*?@(typedef|param|returns|template|callback|property|type)\b/.test(source); +}; + +const publicExportSurface = collectPublicExportSurface(); +const publicExportSurfaceRelative = new Set([...publicExportSurface].map(toRepoRelative)); +const publicJSDocFiles = [...publicExportSurface].filter(hasJSDocTypeSurface).sort(); +const checkedFileSet = new Set(CHECKED_FILES); +const nonPublicCheckedFiles = CHECKED_FILES.filter( + (rel) => !publicExportSurfaceRelative.has(rel) && !REACHABILITY_EXEMPT_CHECKED_FILES.has(rel), +); + +if (nonPublicCheckedFiles.length > 0) { + console.error('[check-jsdoc] gated files are not reachable from the public superdoc export surface:'); + for (const f of nonPublicCheckedFiles) console.error(` - ${f}`); + console.error('Gated JSDoc files must be exported from superdoc, superdoc/super-editor, or superdoc/ui.'); + process.exit(1); +} + // Pre-flight: every file in CHECKED_FILES must opt into `// @ts-check`. // The project's tsconfig sets `checkJs: false`, so a JS file without the // directive is not type-checked at all. Without this guard, removing or @@ -59,7 +250,7 @@ const tsconfigPath = path.join(packageDir, 'tsconfig.json'); const missingDirective = []; const missingFiles = []; for (const rel of CHECKED_FILES) { - const abs = path.join(packageDir, rel); + const abs = path.join(repoRoot, rel); if (!fs.existsSync(abs)) { missingFiles.push(rel); continue; @@ -128,7 +319,7 @@ if (result.status !== 0 && allErrors.length === 0) { process.exit(1); } -const checkedAbsolute = CHECKED_FILES.map((rel) => path.join(packageDir, rel)); +const checkedAbsolute = CHECKED_FILES.map((rel) => path.join(repoRoot, rel)); const isCheckedError = (line) => { const match = line.match(/^([^(]+)\(\d+,\d+\):/); @@ -139,17 +330,20 @@ const isCheckedError = (line) => { const checkedErrors = allErrors.filter(isCheckedError); -console.log('[check-jsdoc] SD-2863 public-contract checkJs gate'); +console.log('[check-jsdoc] SD-2833 public-contract checkJs gate'); console.log('='.repeat(72)); +console.log(`Public JSDoc files discovered: ${publicJSDocFiles.length}`); console.log(`Files under gate: ${CHECKED_FILES.length}`); for (const f of CHECKED_FILES) { console.log(` - ${f}`); } +const ungatedPublicJSDocCount = publicJSDocFiles.filter((abs) => !checkedFileSet.has(toRepoRelative(abs))).length; +console.log(`Ungated public JSDoc files: ${ungatedPublicJSDocCount}`); console.log(); if (checkedErrors.length === 0) { console.log(`OK ${CHECKED_FILES.length} gated file${CHECKED_FILES.length === 1 ? '' : 's'} clean.`); - console.log(` (${allErrors.length} non-gated error${allErrors.length === 1 ? '' : 's'} in the wider tsc run, ignored — see SD-2863 follow-up tickets.)`); + console.log(` (${allErrors.length} non-gated error${allErrors.length === 1 ? '' : 's'} in the wider tsc run, ignored — see SD-2863/SD-2833 follow-ups.)`); process.exit(0); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18c9c93495..3a6b4fbbd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3013,6 +3013,9 @@ importers: '@types/react-dom': specifier: 'catalog:' version: 19.2.3(@types/react@19.2.14) + '@types/uuid': + specifier: 'catalog:' + version: 9.0.8 '@vitejs/plugin-vue': specifier: 'catalog:' version: 6.0.2(rolldown-vite@7.3.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) From 59cf79a530b7157e6893913b995ffd5541f278d8 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Mon, 4 May 2026 17:35:06 -0300 Subject: [PATCH 2/2] test(types): cover track change helper calls (SD-2833) --- .../src/track-changes-helpers.ts | 38 +++++++++++++++++++ tests/consumer-typecheck/typecheck-matrix.mjs | 33 ++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/consumer-typecheck/src/track-changes-helpers.ts diff --git a/tests/consumer-typecheck/src/track-changes-helpers.ts b/tests/consumer-typecheck/src/track-changes-helpers.ts new file mode 100644 index 0000000000..720b7a130e --- /dev/null +++ b/tests/consumer-typecheck/src/track-changes-helpers.ts @@ -0,0 +1,38 @@ +/** + * Consumer typecheck: track changes helper call shapes. + * + * These helpers are public via `superdoc/super-editor`. The calls below guard + * the runtime-valid shapes that SD-2892 fixed after JSDoc had over-tightened + * the generated declarations. + */ +import { trackChangesHelpers } from 'superdoc/super-editor'; + +type IsAny = 0 extends 1 & T ? true : false; +type AssertNotAny = IsAny extends true ? never : true; + +type MarkInsertionOptions = Parameters[0]; +type MarkDeletionOptions = Parameters[0]; +type AddMarkStepOptions = Parameters[0]; + +const _realTrackChangesHelpers: AssertNotAny = true; +const _realMarkInsertionOptions: AssertNotAny = true; +const _realMarkDeletionOptions: AssertNotAny = true; +const _realAddMarkStepOptions: AssertNotAny = true; + +declare const tr: MarkInsertionOptions['tr']; +declare const state: AddMarkStepOptions['state']; +declare const step: AddMarkStepOptions['step']; +declare const newTr: AddMarkStepOptions['newTr']; +declare const doc: AddMarkStepOptions['doc']; + +const user = { name: 'Type Test', email: 'type-test@example.com' }; +const date = '2026-05-04T00:00:00.000Z'; + +trackChangesHelpers.markInsertion({ tr, from: 1, to: 2, user, date }); +trackChangesHelpers.markDeletion({ tr, from: 1, to: 2, user, date }); +trackChangesHelpers.addMarkStep({ state, step, newTr, doc, user, date }); + +void _realTrackChangesHelpers; +void _realMarkInsertionOptions; +void _realMarkDeletionOptions; +void _realAddMarkStepOptions; diff --git a/tests/consumer-typecheck/typecheck-matrix.mjs b/tests/consumer-typecheck/typecheck-matrix.mjs index 4866953fb1..b46a673a33 100644 --- a/tests/consumer-typecheck/typecheck-matrix.mjs +++ b/tests/consumer-typecheck/typecheck-matrix.mjs @@ -304,6 +304,39 @@ const scenarios = [ files: ['src/prosemirror-coexistence.ts'], mustPass: true, }, + // SD-2833: trackChangesHelpers are public through `superdoc/super-editor`. + // These targeted scenarios guard runtime-valid call shapes that `@ts-check` + // does not reject when JSDoc over-tightens generated declarations. + { + name: 'bundler / track changes helper call shapes (SD-2833)', + module: 'ESNext', + moduleResolution: 'bundler', + skipLibCheck: false, + strict: true, + noPropertyAccessFromIndexSignature: true, + files: ['src/track-changes-helpers.ts'], + mustPass: true, + }, + { + name: 'node16 / track changes helper call shapes (SD-2833)', + module: 'Node16', + moduleResolution: 'node16', + skipLibCheck: false, + strict: true, + noPropertyAccessFromIndexSignature: true, + files: ['src/track-changes-helpers.ts'], + mustPass: true, + }, + { + name: 'nodenext / track changes helper call shapes (SD-2833)', + module: 'NodeNext', + moduleResolution: 'nodenext', + skipLibCheck: false, + strict: true, + noPropertyAccessFromIndexSignature: true, + files: ['src/track-changes-helpers.ts'], + mustPass: true, + }, // SD-2892: full public-facing surface with skipLibCheck=false. These // scenarios pack SuperDoc, install it into the consumer fixture, and compile // every public consumer assertion under the resolution modes customers use.