diff --git a/packages/super-editor/package.json b/packages/super-editor/package.json index 5f887ce004..fe9c373ab3 100644 --- a/packages/super-editor/package.json +++ b/packages/super-editor/package.json @@ -8,7 +8,7 @@ ], "exports": { ".": { - "source": "./src/index.js", + "source": "./src/index.ts", "types": "./dist/src/index.d.ts", "import": "./dist/super-editor.es.js" }, @@ -56,7 +56,7 @@ "import": "./dist/style.css" }, "./presentation-editor": { - "source": "./src/index.js", + "source": "./src/index.ts", "import": "./dist/super-editor.es.js", "types": "./dist/index.d.ts" } diff --git a/packages/super-editor/src/core/Editor.d.ts b/packages/super-editor/src/core/Editor.d.ts deleted file mode 100644 index 1ab290d901..0000000000 --- a/packages/super-editor/src/core/Editor.d.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { EditorView } from 'prosemirror-view'; -import type { EditorState } from 'prosemirror-state'; -import type { Schema } from 'prosemirror-model'; -import type { EditorCommands, ChainedCommand, CanObject } from './types/ChainedCommands.js'; - -/** - * Minimal type definition for Editor class - * This provides TypeScript with the structure of the JavaScript Editor class - */ -export declare class Editor { - /** ProseMirror view instance */ - view?: EditorView; - - /** ProseMirror schema */ - schema?: Schema; - - /** Editor converter for import/export */ - converter?: any; - - /** Presentation editor instance for pages mode */ - presentationEditor?: { - element?: HTMLElement; - [key: string]: any; - }; - - /** Editor options passed during construction */ - options?: { - element?: HTMLElement; - [key: string]: any; - }; - - /** Current editor state */ - state?: EditorState; - - /** Update page style (for pages mode) */ - updatePageStyle?: (styles: Record) => void; - - /** Get current page styles (for pages mode) */ - getPageStyles?: () => Record; - - /** Get coordinates at a document position */ - coordsAtPos?: (pos: number) => { left: number; top: number } | undefined; - - /** - * Command service - provides access to all editor commands. - * Use `editor.commands.toggleBold()` to execute commands. - */ - commands: EditorCommands; - - /** - * Create a chain of commands to call multiple commands at once. - * Commands are executed in order when `.run()` is called. - */ - chain(): ChainedCommand; - - /** - * Check if a command or chain of commands can be executed without executing it. - */ - can(): CanObject; - - /** - * Get the maximum content size based on page dimensions and margins. - * When the cursor is inside a table cell, the max width is constrained to that - * cell's width so that newly inserted images are never wider than their containing cell. - * Returns empty object in web layout mode or when no page size is available. - */ - getMaxContentSize(): { width?: number; height?: number }; - - [key: string]: any; -} diff --git a/packages/super-editor/src/core/presentation-editor/types.ts b/packages/super-editor/src/core/presentation-editor/types.ts index a29bb88e85..0328fcb44c 100644 --- a/packages/super-editor/src/core/presentation-editor/types.ts +++ b/packages/super-editor/src/core/presentation-editor/types.ts @@ -388,6 +388,15 @@ export type RangeRect = { height: number; }; +export type BoundingRect = { + top: number; + left: number; + bottom: number; + right: number; + width: number; + height: number; +}; + export type HeaderFooterMode = 'body' | 'header' | 'footer'; export type HeaderFooterSession = { diff --git a/packages/super-editor/src/core/types/EditorTypes.ts b/packages/super-editor/src/core/types/EditorTypes.ts index 5ae4df3c30..be76fd6518 100644 --- a/packages/super-editor/src/core/types/EditorTypes.ts +++ b/packages/super-editor/src/core/types/EditorTypes.ts @@ -85,6 +85,20 @@ export interface Toolbar { [key: string]: unknown; } +/** + * Binary data accepted by document open/import APIs. + */ +export type BinaryData = ArrayBuffer | ArrayBufferView; + +/** + * Represents an unsupported HTML element dropped during paste. + */ +export type UnsupportedContentItem = { + tagName: string; + outerHTML: string; + count: number; +}; + /** * Re-export commonly used types */ diff --git a/packages/super-editor/src/extensions/comment/comments-plugin.d.ts b/packages/super-editor/src/extensions/comment/comments-plugin.d.ts index 61642650c4..3326f29567 100644 --- a/packages/super-editor/src/extensions/comment/comments-plugin.d.ts +++ b/packages/super-editor/src/extensions/comment/comments-plugin.d.ts @@ -1 +1,2 @@ export const CommentsPluginKey: any; +export function createOrUpdateTrackedChangeComment(options: any): any; diff --git a/packages/super-editor/src/index.d.ts b/packages/super-editor/src/index.d.ts deleted file mode 100644 index 2eb66a01bd..0000000000 --- a/packages/super-editor/src/index.d.ts +++ /dev/null @@ -1,1140 +0,0 @@ -/** - * Main type declarations for @superdoc/super-editor - * This file provides TypeScript types for the JavaScript exports in index.js - */ - -export type { EditorView } from 'prosemirror-view'; -export type { EditorState, Transaction } from 'prosemirror-state'; -export type { Schema } from 'prosemirror-model'; -export type { ResolveRangeOutput, DocumentApi } from '@superdoc/document-api'; - -/** - * An opaque, session-local handle representing a captured editor selection. - * - * The handle's bookmark is automatically mapped through every transaction. - * Resolve it via `editor.resolveSelectionHandle(handle)` to get a fresh - * `ResolveRangeOutput` reflecting the current document state. - * - * For deferred UI flows (AI, confirmation dialogs, async chains). - * For immediate mutations, use the snapshot convenience methods instead. - */ -export type SelectionHandle = { - readonly id: number; - readonly surface: 'body' | 'header' | 'footer'; - readonly wasNonEmpty: boolean; - /** @internal Opaque owner reference — do not use directly. */ - readonly _owner: unknown; -}; - -/** - * Bundles the active editing surface's editor, document API, surface label, - * and resolved selection range into a single coherent object. - * - * Returned by `PresentationEditor.getEffectiveSelectionContext()`, - * `PresentationEditor.getCurrentSelectionContext()`, and - * `PresentationEditor.resolveSelectionHandle()`. - */ -export type SelectionCommandContext = { - editor: Editor; - doc: DocumentApi; - surface: 'body' | 'header' | 'footer'; - range: ResolveRangeOutput; -}; - -// ============================================ -// COMMAND TYPES (inlined from ChainedCommands.ts) -// ============================================ - -/** - * Map of built-in command names to their parameter signatures. - * Extensions can augment this interface to add more precise types. - */ -export interface CoreCommandMap {} - -/** - * Map of extension command names to their parameter signatures. - * Extensions should augment this interface via module augmentation. - */ -export interface ExtensionCommandMap {} - -/** - * Props passed to command functions - */ -export interface CommandProps { - editor: Editor; - tr: Transaction; - state: EditorState; - view: EditorView; - dispatch?: (tr: Transaction) => void; -} - -/** - * A command function signature - */ -export type Command = (props: CommandProps) => boolean; - -/** - * Chainable command object returned by editor.chain() - */ -export interface ChainableCommandObject { - run: () => boolean; - [commandName: string]: ((...args: any[]) => ChainableCommandObject) | (() => boolean); -} - -/** - * Chained command type - */ -export type ChainedCommand = ChainableCommandObject; - -/** - * Object returned by editor.can() - */ -export interface CanObject { - chain: () => ChainableCommandObject; - [commandName: string]: ((...args: any[]) => boolean) | (() => ChainableCommandObject); -} - -/** - * All available editor commands. - * Commands are dynamically populated from extensions. - */ -export interface EditorCommands { - // Core commands - focus: (position?: 'start' | 'end' | 'all' | number | boolean | null) => boolean; - blur: () => boolean; - - // Formatting commands (from extensions) - toggleBold: () => boolean; - toggleItalic: () => boolean; - toggleUnderline: () => boolean; - toggleStrike: () => boolean; - toggleHighlight: (color?: string) => boolean; - - // Font commands - setFontSize: (size: string | number) => boolean; - setFontFamily: (family: string) => boolean; - setTextColor: (color: string) => boolean; - - // Alignment commands - setTextAlign: (alignment: 'left' | 'center' | 'right' | 'justify') => boolean; - - // List commands - toggleBulletList: () => boolean; - toggleOrderedList: () => boolean; - - // History commands - undo: () => boolean; - redo: () => boolean; - - // Link commands - setLink: (attrs: { href: string; target?: string }) => boolean; - unsetLink: () => boolean; - - // Table commands - insertTable: (options?: { rows?: number; cols?: number }) => boolean; - deleteTable: () => boolean; - addRowBefore: () => boolean; - addRowAfter: () => boolean; - addColumnBefore: () => boolean; - addColumnAfter: () => boolean; - deleteRow: () => boolean; - deleteColumn: () => boolean; - mergeCells: () => boolean; - splitCell: () => boolean; - - // Image commands - insertImage: (attrs: { src: string; alt?: string }) => boolean; - - // Selection commands - selectAll: () => boolean; - - // Content commands - insertContent: (content: any) => boolean; - setContent: (content: any) => boolean; - clearContent: () => boolean; - - // Allow any other command (for extension commands) - [commandName: string]: ((...args: any[]) => boolean) | undefined; -} - -// ============================================ -// DATA TYPES -// ============================================ - -/** An unsupported HTML element that was dropped during import. */ -export interface UnsupportedContentItem { - /** The tag name, e.g. "HR", "DETAILS" */ - tagName: string; - /** The outerHTML of the element (truncated to 200 chars) */ - outerHTML: string; - /** How many instances of this tag were dropped */ - count: number; -} - -/** Binary data source (works in both browser and Node.js - Buffer extends Uint8Array) */ -export type BinaryData = ArrayBuffer | ArrayBufferView; - -export interface DocxFileEntry { - name: string; - content: string; -} - -export interface OpenOptions { - mode?: 'docx' | 'text' | 'html'; - html?: string; - markdown?: string; - json?: object | null; - isCommentsEnabled?: boolean; - suppressDefaultDocxStyles?: boolean; - documentMode?: 'editing' | 'viewing' | 'suggesting'; - /** - * Allow text selection in viewing mode. - * When true, users can select and copy text while in viewing mode, - * but editing (typing, paste, delete) remains blocked. - * @default false - */ - allowSelectionInViewMode?: boolean; - content?: unknown; - mediaFiles?: Record; - fonts?: Record; - /** Password for opening encrypted .docx files. Cleared from memory after use. */ - password?: string; -} - -// ============================================ -// PRESENTATION EDITOR TYPES -// ============================================ - -/** Page dimensions in points (72 points = 1 inch) */ -export interface PageSize { - /** Width in points */ - w: number; - /** Height in points */ - h: number; -} - -/** Page margin configuration in points */ -export interface PageMargins { - top?: number; - right?: number; - bottom?: number; - left?: number; - header?: number; - footer?: number; -} - -/** Virtualization options for large documents */ -export interface VirtualizationOptions { - enabled?: boolean; - window?: number; - overscan?: number; - gap?: number; - paddingTop?: number; -} - -/** Tracked changes display mode */ -export type TrackedChangesMode = 'review' | 'original' | 'final' | 'off'; - -/** Override tracked changes behavior */ -export interface TrackedChangesOverrides { - mode?: TrackedChangesMode; - enabled?: boolean; -} - -/** Layout mode for page rendering */ -export type LayoutMode = 'vertical' | 'horizontal'; - -/** Remote user presence information */ -export interface RemoteUserInfo { - name?: string; - email?: string; - color: string; -} - -/** Remote cursor state for collaboration */ -export interface RemoteCursorState { - clientId: number; - user: RemoteUserInfo; - anchor: number; - head: number; - updatedAt: number; -} - -/** Presence rendering options */ -export interface PresenceOptions { - enabled?: boolean; - showLabels?: boolean; - maxVisible?: number; - labelFormatter?: (user: RemoteUserInfo) => string; - highlightOpacity?: number; - staleTimeout?: number; -} - -/** Layout engine configuration */ -export interface LayoutEngineOptions { - pageSize?: PageSize; - margins?: PageMargins; - zoom?: number; - virtualization?: VirtualizationOptions; - pageStyles?: Record; - debugLabel?: string; - layoutMode?: LayoutMode; - trackedChanges?: TrackedChangesOverrides; - emitCommentPositionsInViewing?: boolean; - enableCommentsInViewing?: boolean; - presence?: PresenceOptions; - ruler?: { - enabled?: boolean; - interactive?: boolean; - onMarginChange?: (side: 'left' | 'right', marginInches: number) => void; - }; -} - -/** Options for creating a PresentationEditor instance */ -export interface PresentationEditorOptions { - /** Host element where the layout-engine powered UI should render (required) */ - element: HTMLElement; - /** Layout-specific configuration */ - layoutEngineOptions?: LayoutEngineOptions; - /** Document mode: 'editing', 'viewing', or 'suggesting' */ - documentMode?: 'editing' | 'viewing' | 'suggesting'; - /** Collaboration provider with awareness support */ - collaborationProvider?: { - awareness?: unknown; - disconnect?: () => void; - } | null; - /** Whether to disable the context menu */ - disableContextMenu?: boolean; - /** Document content */ - content?: string | object; - /** Editor extensions */ - extensions?: any[]; - /** Whether the editor is editable */ - editable?: boolean; - /** Additional options passed to the underlying Editor */ - [key: string]: any; -} - -/** Layout error information */ -export interface LayoutError { - phase: 'initialization' | 'render'; - error: Error; - timestamp: number; -} - -/** Rectangle with page context */ -export interface RangeRect { - pageIndex: number; - left: number; - right: number; - top: number; - bottom: number; - width: number; - height: number; -} - -/** Layout metrics for telemetry */ -export interface LayoutMetrics { - durationMs: number; - blockCount: number; - pageCount: number; -} - -/** Position hit result from coordinate mapping */ -export interface PositionHit { - pos: number; - layoutEpoch: number; - blockId: string; - pageIndex: number; - column: number; - lineIndex: number; -} - -/** Bounding rectangle dimensions */ -export interface BoundingRect { - top: number; - left: number; - bottom: number; - right: number; - width: number; - height: number; -} - -/** A fragment positioned on a page */ -export interface LayoutFragment { - pmStart: number; - pmEnd: number; - x: number; - y: number; - width: number; - height: number; - blockId: string; - column?: number; -} - -/** A rendered page in the layout */ -export interface LayoutPage { - number: number; - fragments: LayoutFragment[]; - margins?: PageMargins; - size?: PageSize; - orientation?: 'portrait' | 'landscape'; - sectionIndex?: number; - footnoteReserved?: number; -} - -/** Final layout output from the layout engine */ -export interface Layout { - pageSize: PageSize; - pages: LayoutPage[]; - pageGap?: number; - layoutEpoch?: number; -} - -/** A block in the flow document model */ -export interface FlowBlock { - id: string; - type: string; - pmStart: number; - pmEnd: number; - [key: string]: unknown; -} - -/** Measurement data for a block */ -export interface Measure { - blockId: string; - width: number; - height: number; - lines?: Array<{ - width: number; - ascent: number; - descent: number; - lineHeight: number; - }>; - [key: string]: unknown; -} - -/** Section metadata for multi-section documents */ -export interface SectionMetadata { - sectionIndex: number; - startPage: number; - endPage: number; - [key: string]: unknown; -} - -/** Paint snapshot for debugging/testing */ -export interface PaintSnapshot { - formatVersion: 1; - pageCount: number; - lineCount: number; - markerCount: number; - tabCount: number; - pages: Array<{ - index: number; - pageNumber?: number; - lineCount: number; - lines: Array<{ - index: number; - inTableFragment: boolean; - inTableParagraph: boolean; - style: Record; - }>; - }>; -} - -/** Payload for layout update events */ -export interface LayoutUpdatePayload { - blocks: FlowBlock[]; - measures: Measure[]; - layout: Layout; - metrics?: LayoutMetrics; -} - -// ============================================ -// EDITOR CLASS -// ============================================ - -/** - * The main Editor class for SuperDoc. - * Provides a rich text editing experience built on ProseMirror. - */ -export declare class Editor { - /** - * Creates a new Editor instance. - * @param options - Editor configuration options - */ - constructor(options?: { - element?: HTMLElement; - content?: string | object; - extensions?: any[]; - editable?: boolean; - allowSelectionInViewMode?: boolean; - autofocus?: boolean | 'start' | 'end' | 'all' | number; - [key: string]: any; - }); - - /** Load and parse a DOCX file into XML data for headless processing. */ - static loadXmlData( - fileSource: File | Blob | BinaryData, - isNode?: boolean, - options?: { password?: string }, - ): Promise< - | [DocxFileEntry[], Record, Record, Record, Uint8Array | null] - | undefined - >; - - /** Open a document with smart defaults. */ - static open( - source?: string | File | Blob | BinaryData, - config?: Partial<{ - element?: HTMLElement; - selector?: string; - [key: string]: any; - }> & - OpenOptions, - ): Promise; - - /** ProseMirror view instance (undefined in headless mode) */ - view?: EditorView; - - /** ProseMirror schema */ - schema: Schema; - - /** Editor converter for import/export */ - converter?: any; - - /** Presentation editor instance for pages mode */ - presentationEditor?: { - element?: HTMLElement; - [key: string]: any; - }; - - /** Editor options passed during construction */ - options: { - element?: HTMLElement; - [key: string]: any; - }; - - /** Current editor state */ - state: EditorState; - - /** Whether the editor is currently editable */ - isEditable: boolean; - - /** Whether the editor has been destroyed */ - isDestroyed: boolean; - - /** Update page style (for pages mode) */ - updatePageStyle?: (styles: Record) => void; - - /** Get current page styles (for pages mode) */ - getPageStyles?: () => Record; - - /** Get coordinates at a document position */ - coordsAtPos?: (pos: number) => { left: number; top: number } | undefined; - - /** Get the DOM element for a document position */ - getElementAtPos?: ( - pos: number, - options?: { forceRebuild?: boolean; fallbackToCoords?: boolean }, - ) => HTMLElement | null; - - /** - * Command service - provides access to all editor commands. - * @example - * editor.commands.toggleBold(); - * editor.commands.setFontSize('14pt'); - */ - commands: EditorCommands; - - /** - * Create a chain of commands to call multiple commands at once. - * Commands are executed in order when `.run()` is called. - * @example - * editor.chain().toggleBold().toggleItalic().run(); - */ - chain(): ChainedCommand; - - /** - * Check if a command or chain of commands can be executed without executing it. - * @example - * if (editor.can().toggleBold()) { - * // Bold can be toggled - * } - */ - can(): CanObject; - - /** Dispatch a transaction to update editor state (use this in headless mode instead of view.dispatch). */ - dispatch(tr: Transaction): void; - - /** - * Destroy the editor instance and clean up resources. - */ - destroy(): void; - - /** - * Get the current document as HTML. - */ - getHTML(): string; - - /** - * Get the current document as JSON. - */ - getJSON(): object; - - /** - * Get the current document as plain text. - */ - getText(): string; - - /** - * Check if the document is empty. - */ - isEmpty: boolean; - - // --- Tracked selection handle API --- - - /** Capture the live PM selection as a tracked handle. Local-only. */ - captureCurrentSelectionHandle(surface?: 'body' | 'header' | 'footer'): SelectionHandle; - - /** Capture the "effective" selection as a tracked handle. Local-only. */ - captureEffectiveSelectionHandle(surface?: 'body' | 'header' | 'footer'): SelectionHandle; - - /** - * Resolve a previously captured handle into a fresh `ResolveRangeOutput`. - * Returns `null` if the handle was released or the selection collapsed. - */ - resolveSelectionHandle(handle: SelectionHandle): ResolveRangeOutput | null; - - /** Release a tracked selection handle. */ - releaseSelectionHandle(handle: SelectionHandle): void; - - // --- Snapshot convenience API --- - - /** Snapshot convenience: resolve the live PM selection immediately. Local-only. */ - getCurrentSelectionRange(): ResolveRangeOutput; - - /** Snapshot convenience: resolve the "effective" selection immediately. Local-only. */ - getEffectiveSelectionRange(): ResolveRangeOutput; - - /** Allow additional properties */ - [key: string]: any; -} - -// ============================================ -// OTHER CLASSES -// ============================================ - -export declare class SuperConverter { - [key: string]: any; -} - -export declare class DocxZipper { - [key: string]: any; -} - -export declare class SuperToolbar { - [key: string]: any; -} - -/** - * PresentationEditor provides a paginated, layout-engine-powered editing experience. - * It wraps a hidden ProseMirror Editor and renders via the layout engine pipeline. - */ -export declare class PresentationEditor { - /** - * Creates a new PresentationEditor instance. - * @param options - Configuration options including the host element - */ - constructor(options: PresentationEditorOptions); - - /** - * Get a PresentationEditor instance by document ID. - */ - static getInstance(documentId: string): PresentationEditor | undefined; - - /** - * Set zoom globally across all PresentationEditor instances. - */ - static setGlobalZoom(zoom: number): void; - - // ============================================ - // Public Getters - // ============================================ - - /** The underlying ProseMirror Editor instance */ - readonly editor: Editor; - - /** The visible host element where the editor is rendered */ - readonly element: HTMLElement; - - /** Command service for the currently active editor (body or header/footer) */ - readonly commands: EditorCommands; - - /** ProseMirror editor state for the currently active editor */ - readonly state: EditorState; - - /** Whether the active editor accepts input */ - readonly isEditable: boolean; - - /** Editor options for the currently active editor */ - readonly options: Record; - - /** The visible host container element */ - readonly visibleHost: HTMLElement; - - /** Selection overlay element for caret and highlight rendering */ - readonly overlayElement: HTMLElement | null; - - /** Current zoom level (1 = 100%) */ - readonly zoom: number; - - // ============================================ - // Public Methods - // ============================================ - - /** - * Dispatch a ProseMirror transaction to the currently active editor. - */ - dispatch(tr: Transaction): void; - - /** - * Focus the editor. - */ - focus(): void; - - /** - * Returns the currently active editor (body or header/footer session). - */ - getActiveEditor(): Editor; - - // --- Tracked selection handle API --- - - /** Capture the live PM selection on the active editor as a tracked handle. */ - captureCurrentSelectionHandle(): SelectionHandle; - - /** Capture the "effective" selection on the active editor as a tracked handle. */ - captureEffectiveSelectionHandle(): SelectionHandle; - - /** - * Resolve a captured handle into a `SelectionCommandContext`. - * Returns `null` if the handle was released or the selection collapsed. - */ - resolveSelectionHandle(handle: SelectionHandle): SelectionCommandContext | null; - - /** Release a tracked selection handle. */ - releaseSelectionHandle(handle: SelectionHandle): void; - - // --- Snapshot convenience API --- - - /** Snapshot convenience: resolve the live PM selection immediately. Routes through active editor. */ - getCurrentSelectionRange(): ResolveRangeOutput; - - /** Snapshot convenience: resolve the "effective" selection immediately. Routes through active editor. */ - getEffectiveSelectionRange(): ResolveRangeOutput; - - /** Snapshot convenience: current selection + active editing context. */ - getCurrentSelectionContext(): SelectionCommandContext; - - /** Snapshot convenience: effective selection + active editing context. The canonical layout-mode API. */ - getEffectiveSelectionContext(): SelectionCommandContext; - - /** - * Undo the last action in the active editor. - */ - undo(): boolean; - - /** - * Redo the last undone action in the active editor. - */ - redo(): boolean; - - /** - * Run a callback against the active editor. - */ - dispatchInActiveEditor(callback: (editor: Editor) => void): void; - - /** - * Set the document mode and update editor editability. - * @param mode - 'editing', 'viewing', or 'suggesting' - */ - setDocumentMode(mode: 'editing' | 'viewing' | 'suggesting'): void; - - /** - * Override tracked-changes rendering preferences. - */ - setTrackedChangesOverrides(overrides?: TrackedChangesOverrides): void; - - /** - * Update viewing-mode comment rendering behavior. - */ - setViewingCommentOptions(options?: { - emitCommentPositionsInViewing?: boolean; - enableCommentsInViewing?: boolean; - }): void; - - /** - * Toggle the custom context menu. - */ - setContextMenuDisabled(disabled: boolean): void; - - /** - * Subscribe to layout update events. Returns an unsubscribe function. - */ - onLayoutUpdated(handler: (payload: LayoutUpdatePayload) => void): () => void; - - /** - * Subscribe to layout error events. Returns an unsubscribe function. - */ - onLayoutError(handler: (error: LayoutError) => void): () => void; - - /** - * Get the rendered pages. - */ - getPages(): LayoutPage[]; - - /** - * Get the most recent layout error (if any). - */ - getLayoutError(): LayoutError | null; - - /** - * Check if layout is healthy. - */ - isLayoutHealthy(): boolean; - - /** - * Get detailed layout health state. - */ - getLayoutHealthState(): 'healthy' | 'degraded' | 'failed'; - - /** - * Get layout-relative rects for the current document selection. - */ - getSelectionRects(relativeTo?: HTMLElement): RangeRect[]; - - /** - * Convert a document range into layout-based bounding rects. - */ - getRangeRects(from: number, to: number, relativeTo?: HTMLElement): RangeRect[]; - - /** - * Get bounds for a document range. - */ - getSelectionBounds( - from: number, - to: number, - relativeTo?: HTMLElement, - ): { - bounds: BoundingRect; - rects: RangeRect[]; - pageIndex: number; - } | null; - - /** - * Remap comment positions to layout coordinates with bounds and rects. - */ - getCommentBounds( - positions: Record, - relativeTo?: HTMLElement, - ): Record< - string, - { - start?: number; - end?: number; - pos?: number; - bounds?: BoundingRect; - rects?: RangeRect[]; - pageIndex?: number; - [key: string]: unknown; - } - >; - - /** - * Get current layout snapshot. - */ - getLayoutSnapshot(): { - blocks: FlowBlock[]; - measures: Measure[]; - layout: Layout | null; - sectionMetadata: SectionMetadata[]; - }; - - /** - * Get current layout options. - */ - getLayoutOptions(): LayoutEngineOptions; - - /** - * Get current paint snapshot. - */ - getPaintSnapshot(): PaintSnapshot | null; - - /** - * Get section-aware page styles. - */ - getCurrentSectionPageStyles(): { - pageSize: { width: number; height: number }; - pageMargins: { left: number; right: number; top: number; bottom: number }; - sectionIndex: number; - orientation: 'portrait' | 'landscape'; - }; - - /** - * Get remote cursor states for all collaborators. - */ - getRemoteCursors(): RemoteCursorState[]; - - /** - * Set the layout mode (vertical or horizontal). - */ - setLayoutMode(mode: LayoutMode): void; - - /** - * Hit test at client coordinates to find document position. - */ - hitTest(clientX: number, clientY: number): PositionHit | null; - - /** - * Normalize client coordinates to layout coordinates. - */ - normalizeClientPoint( - clientX: number, - clientY: number, - ): { - x: number; - y: number; - pageIndex?: number; - pageLocalY?: number; - } | null; - - /** - * Get viewport coordinates at a document position. - */ - coordsAtPos( - pos: number, - ): { left: number; right: number; top: number; bottom: number; width: number; height: number } | null; - - /** - * Get the painted DOM element for a document position (body only). - */ - getElementAtPos(pos: number, options?: { forceRebuild?: boolean; fallbackToCoords?: boolean }): HTMLElement | null; - - /** - * Scroll to a document position. - */ - scrollToPosition(pos: number, options?: { behavior?: ScrollBehavior; block?: ScrollLogicalPosition }): boolean; - - /** - * Scroll a comment or tracked-change anchor to a viewport Y coordinate. - */ - scrollThreadAnchorToClientY( - threadId: string, - targetClientY: number, - options?: { behavior?: ScrollBehavior }, - ): boolean; - - /** - * Scroll to a document position (async version). - */ - scrollToPositionAsync( - pos: number, - options?: { behavior?: ScrollBehavior; block?: ScrollLogicalPosition }, - ): Promise; - - /** - * Scroll to a specific page number. - */ - scrollToPage(pageNumber: number, scrollBehavior?: ScrollBehavior): Promise; - - /** - * Get document position at viewport coordinates. - */ - posAtCoords(coords: { - left?: number; - top?: number; - clientX?: number; - clientY?: number; - }): { pos: number; inside: number } | null; - - /** - * Update zoom level and re-render. - * @param zoom - Zoom level multiplier (1.0 = 100%) - */ - setZoom(zoom: number): void; - - /** - * Navigate to a document anchor/bookmark. - */ - goToAnchor(anchor: string): Promise; - - /** - * Convert layout coordinates back to viewport coordinates. - */ - denormalizeClientPoint( - layoutX: number, - layoutY: number, - pageIndex?: number, - height?: number, - ): { x: number; y: number; height?: number } | null; - - /** - * Compute caret position in layout coordinates. - */ - computeCaretLayoutRect(pos: number): { pageIndex: number; x: number; y: number; height: number } | null; - - /** - * Clean up editor and DOM nodes. - */ - destroy(): void; - - /** - * Register an event listener. - */ - on(event: string, handler: (...args: any[]) => void): void; - - /** - * Remove an event listener. - */ - off(event: string, handler: (...args: any[]) => void): void; - - /** - * Emit an event. - */ - emit(event: string, ...args: any[]): void; - - /** Allow additional properties */ - [key: string]: any; -} - -// ============================================ -// VUE COMPONENTS -// ============================================ - -export declare const SuperEditor: any; -export declare const SuperInput: any; -export declare const BasicUpload: any; -export declare const Toolbar: any; -export declare const AIWriter: any; -export declare const ContextMenu: any; -/** @deprecated Use ContextMenu instead */ -export declare const SlashMenu: any; - -// ============================================ -// HELPER MODULES -// ============================================ - -export declare const helpers: { - [key: string]: any; -}; - -export declare const fieldAnnotationHelpers: { - [key: string]: any; -}; - -export declare const trackChangesHelpers: { - [key: string]: any; -}; - -export declare const AnnotatorHelpers: { - [key: string]: any; -}; - -export declare const SectionHelpers: { - [key: string]: any; -}; - -export declare const registeredHandlers: { - [key: string]: any; -}; - -// ============================================ -// FUNCTIONS -// ============================================ - -export declare function getMarksFromSelection(selection: any): any[]; -export declare function getActiveFormatting(state: any): Record; -export declare function getStarterExtensions(): any[]; -export declare function getRichTextExtensions(): any[]; -export declare function createZip(files: any): Promise; -export declare function getAllowedImageDimensions(file: File): Promise<{ width: number; height: number }>; - -// ============================================ -// TYPE GUARDS -// ============================================ - -/** - * Type guard to check if a node is of a specific type. - * Narrows the node.attrs type to the specific node's attributes. - */ -export declare function isNodeType( - node: { type: { name: string }; attrs: unknown }, - typeName: T, -): node is { type: { name: T }; attrs: any }; - -/** - * Assert that a node is of a specific type. - * Throws if the node type doesn't match. - */ -export declare function assertNodeType( - node: { type: { name: string }; attrs: unknown }, - typeName: T, -): asserts node is { type: { name: T }; attrs: any }; - -/** - * Type guard to check if a mark is of a specific type. - */ -export declare function isMarkType( - mark: { type: { name: string }; attrs: unknown }, - typeName: T, -): mark is { type: { name: T }; attrs: any }; - -// ============================================ -// EXTENSION HELPERS -// ============================================ - -export declare function defineNode(config: any): any; -export declare function defineMark(config: any): any; - -// ============================================ -// EXTENSIONS NAMESPACE -// ============================================ - -export declare const Extensions: { - Node: any; - Attribute: any; - Extension: any; - Mark: any; - Plugin: any; - PluginKey: any; - Decoration: any; - DecorationSet: any; -}; - -// ============================================ -// PLUGIN KEYS -// ============================================ - -export declare const TrackChangesBasePluginKey: any; -export declare const CommentsPluginKey: any; - -// ============================================ -// ENCRYPTION -// ============================================ - -/** Error codes for OOXML encryption failures. */ -export declare const DocxEncryptionErrorCode: { - readonly PASSWORD_REQUIRED: 'DOCX_PASSWORD_REQUIRED'; - readonly PASSWORD_INVALID: 'DOCX_PASSWORD_INVALID'; - readonly ENCRYPTION_UNSUPPORTED: 'DOCX_ENCRYPTION_UNSUPPORTED'; - readonly DECRYPTION_FAILED: 'DOCX_DECRYPTION_FAILED'; -}; - -export type DocxEncryptionErrorCode = (typeof DocxEncryptionErrorCode)[keyof typeof DocxEncryptionErrorCode]; - -/** Thrown when a DOCX file is encrypted and cannot be processed. */ -export declare class DocxEncryptionError extends Error { - readonly code: DocxEncryptionErrorCode; - readonly cause?: Error; - constructor(code: DocxEncryptionErrorCode, message: string, cause?: Error); -} diff --git a/packages/super-editor/src/index.public-api.test.js b/packages/super-editor/src/index.public-api.test.js index b99c921cab..1fa39e81b9 100644 --- a/packages/super-editor/src/index.public-api.test.js +++ b/packages/super-editor/src/index.public-api.test.js @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'; describe('public root exports', () => { it('does not expose document-api adapter assembly from the package root', () => { - const indexPath = resolve(import.meta.dirname, 'index.js'); + const indexPath = resolve(import.meta.dirname, 'index.ts'); const source = readFileSync(indexPath, 'utf8'); expect(source).not.toMatch(/export\s*\{\s*assembleDocumentApiAdapters\s*\}/); diff --git a/packages/super-editor/src/index.js b/packages/super-editor/src/index.ts similarity index 69% rename from packages/super-editor/src/index.js rename to packages/super-editor/src/index.ts index 84ee3dab05..1708d4394e 100644 --- a/packages/super-editor/src/index.js +++ b/packages/super-editor/src/index.ts @@ -130,3 +130,61 @@ export { /** @internal */ onCollaborationProviderSynced, }; + +// ============================================ +// TYPE RE-EXPORTS +// Auto-generated types — sourced from actual TS implementations +// ============================================ + +// ProseMirror core types +export type { EditorView } from 'prosemirror-view'; +export type { EditorState, Transaction } from 'prosemirror-state'; +export type { Schema } from 'prosemirror-model'; + +// Document API types +export type { ResolveRangeOutput, DocumentApi } from '@superdoc/document-api'; + +// Selection handle types +export type { SelectionHandle } from './core/selection-state.js'; +export type { SelectionCommandContext } from './core/presentation-editor/PresentationEditor.js'; + +// Command types +export type { + EditorCommands, + CommandProps, + Command, + ChainedCommand, + ChainableCommandObject, + CanObject, + CoreCommandMap, + ExtensionCommandMap, +} from './core/types/ChainedCommands.js'; + +// Editor configuration types +export type { DocxFileEntry } from './core/types/EditorConfig.js'; +export type { BinaryData, UnsupportedContentItem } from './core/types/EditorTypes.js'; +export type { OpenOptions, SaveOptions, ExportOptions } from './core/Editor.js'; + +// PresentationEditor public types +export type { + PageSize, + PageMargins, + VirtualizationOptions, + RemoteUserInfo, + RemoteCursorState, + PresenceOptions, + TrackedChangesOverrides, + LayoutEngineOptions, + PresentationEditorOptions, + LayoutMetrics, + LayoutError, + RangeRect, + BoundingRect, + LayoutUpdatePayload, +} from './core/presentation-editor/types.js'; + +// Layout engine types +export type { PositionHit } from '@superdoc/layout-bridge'; +export type { PaintSnapshot, LayoutMode } from '@superdoc/painter-dom'; +export type { FlowBlock, Layout, Measure, SectionMetadata, TrackedChangesMode } from '@superdoc/contracts'; +export type { Page as LayoutPage, Fragment as LayoutFragment } from '@superdoc/contracts'; diff --git a/packages/super-editor/tsconfig.json b/packages/super-editor/tsconfig.json index 063063e81e..6157bad689 100644 --- a/packages/super-editor/tsconfig.json +++ b/packages/super-editor/tsconfig.json @@ -20,6 +20,6 @@ "@translator": ["./src/core/super-converter/v3/node-translator/"] } }, - "include": ["src/**/*"], + "include": ["src/**/*", "../../shared/common/vue-shim.d.ts"], "exclude": ["**/*.test.js", "**/*.test.ts"] } diff --git a/packages/super-editor/vite.config.js b/packages/super-editor/vite.config.js index bb4cdf51f0..7e940a49ef 100644 --- a/packages/super-editor/vite.config.js +++ b/packages/super-editor/vite.config.js @@ -105,7 +105,7 @@ export default defineConfig(({ mode }) => { build: { target: 'es2020', lib: { - entry: "src/index.js", + entry: "src/index.ts", formats: ['es'], name: "super-editor", cssFileName: 'style', @@ -117,7 +117,7 @@ export default defineConfig(({ mode }) => { 'y-protocols', ], input: { - 'super-editor': 'src/index.js', + 'super-editor': 'src/index.ts', 'types': 'src/types.ts', 'editor': '@core/Editor', 'converter': '@core/super-converter/SuperConverter', diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs index feeea68354..96ec183743 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -139,6 +139,18 @@ for (const filePath of dtsFiles) { }); + // Fix .ts extensions in import specifiers → .js + // vite-plugin-dts preserves .ts extensions from the source when the entry + // point is a .ts file. TypeScript expects .js extensions in .d.ts files. + fileContent = fileContent.replace( + /(?<=from\s+['"]|import\(['"])([^'"]+)\.ts(?=['"])/g, + (match, pathWithoutExt) => { + changed = true; + totalReplacements++; + return `${pathWithoutExt}.js`; + }, + ); + if (changed) { fs.writeFileSync(filePath, fileContent); fixedFiles++; @@ -190,8 +202,8 @@ const workspaceImports = new Map(); // module → Set for (const filePath of dtsFiles) { const fileContent = fs.readFileSync(filePath, 'utf8'); - // Match: import { Foo, Bar } from '...' and import type { Foo } from '...' - const namedImports = fileContent.matchAll(/import\s+(?:type\s+)?\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g); + // Match: import/export { Foo, Bar } from '...' and import/export type { Foo } from '...' + const namedImports = fileContent.matchAll(/(?:import|export)\s+(?:type\s+)?\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g); for (const m of namedImports) { const mod = m[2]; @@ -221,7 +233,10 @@ for (const filePath of dtsFiles) { const bareRefs = fileContent.matchAll(/['"](@superdoc\/[^'"]+)['"]/g); for (const m of bareRefs) { const mod = m[1]; - if (mod.startsWith('@superdoc/common') || mod.startsWith('@superdoc/super-editor')) continue; + // Skip @superdoc/super-editor (consumer-facing, not internal) + // Skip @superdoc/common root module (inlined separately), but allow subpath + // imports like @superdoc/common/components/BasicUpload.vue to be shimmed + if (mod === '@superdoc/common' || mod.startsWith('@superdoc/super-editor')) continue; if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set()); } } diff --git a/packages/superdoc/src/index.js b/packages/superdoc/src/index.js index 1fa8493be2..9063618c3b 100644 --- a/packages/superdoc/src/index.js +++ b/packages/superdoc/src/index.js @@ -1,6 +1,8 @@ import { SuperConverter, Editor, + PresentationEditor, + getStarterExtensions, getRichTextExtensions, createZip, Extensions, @@ -10,11 +12,87 @@ import { trackChangesHelpers, AnnotatorHelpers, SectionHelpers, + // Additional runtime exports + DocxZipper, + SuperToolbar, + getMarksFromSelection, + getActiveFormatting, + getAllowedImageDimensions, + isNodeType, + assertNodeType, + isMarkType, + defineNode, + defineMark, + TrackChangesBasePluginKey, + CommentsPluginKey, + // Vue components + SuperEditor, + SuperInput, + BasicUpload, + Toolbar, + AIWriter, + ContextMenu, + SlashMenu, } from '@superdoc/super-editor'; import { DOCX, PDF, HTML, getFileObject, compareVersions } from '@superdoc/common'; import BlankDOCX from '@superdoc/common/data/blank.docx?url'; import { getSchemaIntrospection } from './helpers/schema-introspection.js'; +// ============================================ +// TYPE RE-EXPORTS +// These types are defined in @superdoc/super-editor and re-exported for consumers. +// vite-plugin-dts picks up these JSDoc @typedef imports and generates +// corresponding `export type` declarations in the consumer-facing .d.ts. +// ============================================ + +/** + * @typedef {import('@superdoc/super-editor').EditorState} EditorState + * @typedef {import('@superdoc/super-editor').Transaction} Transaction + * @typedef {import('@superdoc/super-editor').Schema} Schema + * @typedef {import('@superdoc/super-editor').EditorView} EditorView + * @typedef {import('@superdoc/super-editor').EditorCommands} EditorCommands + * @typedef {import('@superdoc/super-editor').ChainedCommand} ChainedCommand + * @typedef {import('@superdoc/super-editor').ChainableCommandObject} ChainableCommandObject + * @typedef {import('@superdoc/super-editor').CommandProps} CommandProps + * @typedef {import('@superdoc/super-editor').Command} Command + * @typedef {import('@superdoc/super-editor').CanObject} CanObject + * @typedef {import('@superdoc/super-editor').PresentationEditorOptions} PresentationEditorOptions + * @typedef {import('@superdoc/super-editor').LayoutEngineOptions} LayoutEngineOptions + * @typedef {import('@superdoc/super-editor').PageSize} PageSize + * @typedef {import('@superdoc/super-editor').PageMargins} PageMargins + * @typedef {import('@superdoc/super-editor').VirtualizationOptions} VirtualizationOptions + * @typedef {import('@superdoc/super-editor').TrackedChangesMode} TrackedChangesMode + * @typedef {import('@superdoc/super-editor').TrackedChangesOverrides} TrackedChangesOverrides + * @typedef {import('@superdoc/super-editor').LayoutMode} LayoutMode + * @typedef {import('@superdoc/super-editor').PresenceOptions} PresenceOptions + * @typedef {import('@superdoc/super-editor').RemoteUserInfo} RemoteUserInfo + * @typedef {import('@superdoc/super-editor').RemoteCursorState} RemoteCursorState + * @typedef {import('@superdoc/super-editor').Layout} Layout + * @typedef {import('@superdoc/super-editor').LayoutPage} LayoutPage + * @typedef {import('@superdoc/super-editor').LayoutFragment} LayoutFragment + * @typedef {import('@superdoc/super-editor').RangeRect} RangeRect + * @typedef {import('@superdoc/super-editor').BoundingRect} BoundingRect + * @typedef {import('@superdoc/super-editor').LayoutError} LayoutError + * @typedef {import('@superdoc/super-editor').LayoutMetrics} LayoutMetrics + * @typedef {import('@superdoc/super-editor').PositionHit} PositionHit + * @typedef {import('@superdoc/super-editor').FlowBlock} FlowBlock + * @typedef {import('@superdoc/super-editor').Measure} Measure + * @typedef {import('@superdoc/super-editor').SectionMetadata} SectionMetadata + * @typedef {import('@superdoc/super-editor').PaintSnapshot} PaintSnapshot + * @typedef {import('@superdoc/super-editor').OpenOptions} OpenOptions + * @typedef {import('@superdoc/super-editor').DocxFileEntry} DocxFileEntry + * @typedef {import('@superdoc/super-editor').BinaryData} BinaryData + * @typedef {import('@superdoc/super-editor').UnsupportedContentItem} UnsupportedContentItem + * @typedef {import('@superdoc/super-editor').SaveOptions} SaveOptions + * @typedef {import('@superdoc/super-editor').ExportOptions} ExportOptions + * @typedef {import('@superdoc/super-editor').SelectionHandle} SelectionHandle + * @typedef {import('@superdoc/super-editor').SelectionCommandContext} SelectionCommandContext + * @typedef {import('@superdoc/super-editor').ResolveRangeOutput} ResolveRangeOutput + * @typedef {import('@superdoc/super-editor').LayoutUpdatePayload} LayoutUpdatePayload + * @typedef {import('@superdoc/super-editor').CoreCommandMap} CoreCommandMap + * @typedef {import('@superdoc/super-editor').ExtensionCommandMap} ExtensionCommandMap + */ + // Public exports export { SuperDoc } from './core/SuperDoc.js'; export { createTheme, buildTheme } from './core/theme/create-theme.ts'; @@ -23,6 +101,8 @@ export { getFileObject, compareVersions, Editor, + PresentationEditor, + getStarterExtensions, getRichTextExtensions, getSchemaIntrospection, @@ -46,4 +126,33 @@ export { Extensions, /** @internal */ registeredHandlers, + + // Additional classes + DocxZipper, + SuperToolbar, + + // Helper functions + getMarksFromSelection, + getActiveFormatting, + getAllowedImageDimensions, + + // Type guards and extension helpers + isNodeType, + assertNodeType, + isMarkType, + defineNode, + defineMark, + + // Plugin keys + TrackChangesBasePluginKey, + CommentsPluginKey, + + // Vue components + SuperEditor, + SuperInput, + BasicUpload, + Toolbar, + AIWriter, + ContextMenu, + SlashMenu, }; diff --git a/packages/superdoc/vite.config.js b/packages/superdoc/vite.config.js index 5b848f3c89..7cfae38e48 100644 --- a/packages/superdoc/vite.config.js +++ b/packages/superdoc/vite.config.js @@ -79,8 +79,8 @@ export const getAliases = (_isDev) => { { find: '@superdoc/super-editor/super-input', replacement: path.resolve(__dirname, '../super-editor/src/components/SuperInput.vue') }, { find: '@superdoc/super-editor/ai-writer', replacement: path.resolve(__dirname, '../super-editor/src/core/components/AIWriter.vue') }, { find: '@superdoc/super-editor/style.css', replacement: path.resolve(__dirname, '../super-editor/src/style.css') }, - { find: '@superdoc/super-editor/presentation-editor', replacement: path.resolve(__dirname, '../super-editor/src/index.js') }, - { find: '@superdoc/super-editor', replacement: path.resolve(__dirname, '../super-editor/src/index.js') }, + { find: '@superdoc/super-editor/presentation-editor', replacement: path.resolve(__dirname, '../super-editor/src/index.ts') }, + { find: '@superdoc/super-editor', replacement: path.resolve(__dirname, '../super-editor/src/index.ts') }, // Map @superdoc/ to ./src/ for internal paths ...superdocSrcAliases.map(name => ({ diff --git a/tests/consumer-typecheck/package.json b/tests/consumer-typecheck/package.json new file mode 100644 index 0000000000..152f359d33 --- /dev/null +++ b/tests/consumer-typecheck/package.json @@ -0,0 +1,16 @@ +{ + "name": "consumer-typecheck", + "private": true, + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "typecheck:matrix": "node typecheck-matrix.mjs" + }, + "dependencies": { + "superdoc": "file:../../packages/superdoc/superdoc.tgz" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.9.3" + } +} diff --git a/tests/consumer-typecheck/src/headless-node.ts b/tests/consumer-typecheck/src/headless-node.ts new file mode 100644 index 0000000000..98d68b411b --- /dev/null +++ b/tests/consumer-typecheck/src/headless-node.ts @@ -0,0 +1,31 @@ +/** + * Consumer typecheck: Node.js headless usage. + * + * Verifies that Buffer is included in export return types + * and that SaveOptions includes all fields (fieldsHighlightColor, + * compression) that headless consumers rely on. + */ +import { Editor } from 'superdoc'; +import type { SaveOptions } from 'superdoc'; + +async function headlessExport() { + const editor = new Editor({}); + + // exportDocument should return Blob | Buffer + const result = await editor.exportDocument({ isFinalDoc: true }); + + // In Node.js, result is Buffer + if (Buffer.isBuffer(result)) { + const fs = await import('fs'); + fs.writeFileSync('/tmp/test.docx', result); + } + + // Save with all options including fieldsHighlightColor and compression + const opts: SaveOptions = { + isFinalDoc: true, + commentsType: 'none', + fieldsHighlightColor: null, + compression: 'STORE', + }; + await editor.exportDocx(opts); +} diff --git a/tests/consumer-typecheck/src/imports-main.ts b/tests/consumer-typecheck/src/imports-main.ts new file mode 100644 index 0000000000..91a16a9129 --- /dev/null +++ b/tests/consumer-typecheck/src/imports-main.ts @@ -0,0 +1,106 @@ +/** + * Consumer typecheck: main "superdoc" entry point. + * + * Exercises every public runtime export and type import. + * If this file compiles, consumers can use these APIs. + */ + +// Runtime imports +import { + Editor, + PresentationEditor, + SuperDoc, + superEditorHelpers, + createTheme, + buildTheme, + getRichTextExtensions, + getStarterExtensions, + Extensions, + createZip, + DOCX, + PDF, + HTML, + getMarksFromSelection, + getActiveFormatting, + getAllowedImageDimensions, + isNodeType, + isMarkType, + defineNode, + defineMark, + TrackChangesBasePluginKey, + CommentsPluginKey, + DocxZipper, + SuperToolbar, +} from 'superdoc'; + +// Type imports +import type { + EditorState, + Transaction, + Schema, + EditorView, + EditorCommands, + ChainableCommandObject, + ChainedCommand, + CommandProps, + Command, + CanObject, + CoreCommandMap, + ExtensionCommandMap, + OpenOptions, + DocxFileEntry, + BinaryData, + SaveOptions, + ExportOptions, + PresentationEditorOptions, + LayoutEngineOptions, + PageSize, + PageMargins, + VirtualizationOptions, + TrackedChangesMode, + TrackedChangesOverrides, + LayoutMode, + PresenceOptions, + RemoteUserInfo, + RemoteCursorState, + Layout, + LayoutPage, + LayoutFragment, + RangeRect, + BoundingRect, + LayoutError, + LayoutMetrics, + PositionHit, + FlowBlock, + Measure, + SectionMetadata, + PaintSnapshot, + LayoutUpdatePayload, + UnsupportedContentItem, + SelectionHandle, + SelectionCommandContext, + ResolveRangeOutput, +} from 'superdoc'; + +// Verify types are usable (not just importable) +const pageSize: PageSize = { w: 612, h: 792 }; +const margins: PageMargins = { top: 72, right: 72, bottom: 72, left: 72 }; +const saveOpts: SaveOptions = { + isFinalDoc: true, + fieldsHighlightColor: '#ff0', + compression: 'DEFLATE', +}; +const binaryData: BinaryData = new ArrayBuffer(10); +const rect: BoundingRect = { + top: 0, + left: 0, + bottom: 100, + right: 100, + width: 100, + height: 100, +}; +const unsupported: UnsupportedContentItem = { + tagName: 'HR', + outerHTML: '
', + count: 1, +}; diff --git a/tests/consumer-typecheck/src/imports-sub-export.ts b/tests/consumer-typecheck/src/imports-sub-export.ts new file mode 100644 index 0000000000..e6969c4413 --- /dev/null +++ b/tests/consumer-typecheck/src/imports-sub-export.ts @@ -0,0 +1,9 @@ +/** + * Consumer typecheck: "superdoc/super-editor" sub-export. + * + * Verifies the facade entry point works for consumers + * who import directly from the super-editor sub-path. + */ +import { Editor, PresentationEditor } from 'superdoc/super-editor'; + +const editor = new Editor({}); diff --git a/tests/consumer-typecheck/typecheck-matrix.mjs b/tests/consumer-typecheck/typecheck-matrix.mjs new file mode 100644 index 0000000000..4c717cfc80 --- /dev/null +++ b/tests/consumer-typecheck/typecheck-matrix.mjs @@ -0,0 +1,193 @@ +/** + * TypeScript compatibility matrix. + * + * Tests that superdoc's type declarations work across all common + * tsconfig combinations consumers might use: + * - moduleResolution: bundler, node16, nodenext + * - skipLibCheck: true (all scenarios), false (regression check) + * - strict: true and false + * - Import paths: "superdoc", "superdoc/super-editor" + * - Node.js headless usage (Buffer return types) + * + * Run: npm run typecheck:matrix + */ + +import { execSync } from 'child_process'; +import { writeFileSync, mkdirSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const scenarios = [ + // Core scenarios — must all pass + { + name: 'bundler / strict / skipLibCheck', + module: 'ESNext', + moduleResolution: 'bundler', + skipLibCheck: true, + strict: true, + files: ['src/imports-main.ts', 'src/imports-sub-export.ts'], + mustPass: true, + }, + { + name: 'node16 / strict / skipLibCheck', + module: 'Node16', + moduleResolution: 'node16', + skipLibCheck: true, + strict: true, + files: ['src/imports-main.ts'], + mustPass: true, + }, + { + name: 'nodenext / strict / skipLibCheck', + module: 'NodeNext', + moduleResolution: 'nodenext', + skipLibCheck: true, + strict: true, + files: ['src/imports-main.ts'], + mustPass: true, + }, + { + name: 'bundler / headless Node.js', + module: 'ESNext', + moduleResolution: 'bundler', + skipLibCheck: true, + strict: true, + files: ['src/headless-node.ts'], + mustPass: true, + }, + { + name: 'node16 / headless Node.js', + module: 'Node16', + moduleResolution: 'node16', + skipLibCheck: true, + strict: true, + files: ['src/headless-node.ts'], + mustPass: true, + }, + { + name: 'bundler / sub-export only', + module: 'ESNext', + moduleResolution: 'bundler', + skipLibCheck: true, + strict: true, + files: ['src/imports-sub-export.ts'], + mustPass: true, + }, + { + name: 'node16 / sub-export only', + module: 'Node16', + moduleResolution: 'node16', + skipLibCheck: true, + strict: true, + files: ['src/imports-sub-export.ts'], + mustPass: true, + }, + { + name: 'bundler / loose (non-strict)', + module: 'ESNext', + moduleResolution: 'bundler', + skipLibCheck: true, + strict: false, + files: ['src/imports-main.ts', 'src/imports-sub-export.ts'], + mustPass: true, + }, + // skipLibCheck=false — informational only (pre-existing dep errors) + { + name: 'bundler / skipLibCheck=false', + module: 'ESNext', + moduleResolution: 'bundler', + skipLibCheck: false, + strict: true, + files: ['src/imports-main.ts'], + mustPass: false, // may fail from dep errors in node_modules + }, +]; + +const tscPath = join(__dirname, 'node_modules', '.bin', 'tsc'); +let passed = 0; +let failed = 0; +let warnings = 0; + +console.log('TypeScript Compatibility Matrix'); +console.log('='.repeat(80)); +console.log(); + +for (const scenario of scenarios) { + const tsconfig = { + compilerOptions: { + target: 'ES2022', + module: scenario.module, + moduleResolution: scenario.moduleResolution, + strict: scenario.strict, + skipLibCheck: scenario.skipLibCheck, + noEmit: true, + esModuleInterop: true, + types: ['node'], + }, + include: scenario.files, + }; + + const tsconfigPath = join(__dirname, 'tsconfig.matrix.json'); + writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2)); + + let output = ''; + let exitCode = 0; + try { + output = execSync(`${tscPath} -p ${tsconfigPath} --noEmit 2>&1`, { + cwd: __dirname, + encoding: 'utf-8', + }); + } catch (e) { + output = e.stdout || ''; + exitCode = e.status || 1; + } + + const srcErrors = (output.match(/^src\//gm) || []).length; + const nmErrors = (output.match(/^node_modules\//gm) || []).length; + + let status; + let icon; + if (exitCode === 0) { + icon = '✓'; + status = 'PASS'; + passed++; + } else if (srcErrors === 0 && !scenario.mustPass) { + icon = '⚠'; + status = `DEPS (nm:${nmErrors})`; + warnings++; + } else if (srcErrors === 0) { + icon = '⚠'; + status = `DEPS (nm:${nmErrors})`; + warnings++; + } else { + icon = '✗'; + status = `FAIL (src:${srcErrors} nm:${nmErrors})`; + failed++; + if (scenario.mustPass) { + console.log(` ${icon} ${scenario.name}: ${status}`); + console.log( + output + .split('\n') + .filter((l) => l.startsWith('src/')) + .map((l) => ` ${l}`) + .join('\n'), + ); + continue; + } + } + + console.log(` ${icon} ${scenario.name}: ${status}`); +} + +console.log(); +console.log('='.repeat(80)); +console.log(`Results: ${passed} passed, ${failed} failed, ${warnings} warnings`); + +if (failed > 0) { + console.log('\nFAILED — consumer types are broken for some configurations'); + process.exit(1); +} else { + console.log('\nAll required scenarios pass.'); +}