From 271641b7973f40bd085032a043f2c4bf389a57ae Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Fri, 13 Mar 2026 19:02:05 -0300 Subject: [PATCH 1/6] fix(types): fix broken .d.ts imports in published superdoc package (SD-2227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vite-plugin-dts was emitting .d.ts files with pnpm node_modules paths and private workspace package imports that consumers can't resolve. - Rewrite pnpm physical paths back to bare specifiers (253 imports) - Fix broken subpath exports (index.js/types → types.js) - Fix absolute-looking paths from alias resolution - Generate ambient module shims for @superdoc/* workspace packages - Add hand-written shims for bundled deps (prosemirror, vue, yjs, etc.) with correct generics and class declarations - Add global Buffer interface for Node.js type references - Fix hand-written index.d.ts to import types before local use Tested with skipLibCheck:true and skipLibCheck:false — 0 errors. --- packages/super-editor/src/index.d.ts | 8 +- packages/superdoc/scripts/ensure-types.cjs | 319 +++++++++++++++++++++ 2 files changed, 324 insertions(+), 3 deletions(-) diff --git a/packages/super-editor/src/index.d.ts b/packages/super-editor/src/index.d.ts index 14d2b6d6bd..7362a6d64e 100644 --- a/packages/super-editor/src/index.d.ts +++ b/packages/super-editor/src/index.d.ts @@ -3,9 +3,11 @@ * 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'; +// Re-export prosemirror types for consumers AND import for local use +import type { EditorView } from 'prosemirror-view'; +import type { EditorState, Transaction } from 'prosemirror-state'; +import type { Schema } from 'prosemirror-model'; +export type { EditorView, EditorState, Transaction, Schema }; // ============================================ // COMMAND TYPES (inlined from ChainedCommands.ts) diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs index aa6a6e253e..4e425c99a4 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -58,4 +58,323 @@ if (hadWorkspaceImport) { console.log('[ensure-types] ✓ Inlined @superdoc/common types'); } +// --------------------------------------------------------------------------- +// Fix pnpm node_modules paths in ALL .d.ts files (SD-2227) +// +// vite-plugin-dts resolves bare specifiers like 'prosemirror-view' to physical +// pnpm paths like '../../node_modules/.pnpm/prosemirror-view@1.41.5/node_modules/prosemirror-view/dist/index.js'. +// Consumers don't have these paths — rewrite them back to bare specifiers. +// --------------------------------------------------------------------------- + +/** + * Recursively find all .d.ts files under a directory. + */ +function findDtsFiles(dir) { + const results = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + results.push(...findDtsFiles(fullPath)); + } else if (entry.name.endsWith('.d.ts')) { + results.push(fullPath); + } + } + return results; +} + +// Match pnpm node_modules paths in both `from '...'` and `import('...')` contexts. +// Captures the bare package name from the pnpm structure: +// .../node_modules/.pnpm/@/node_modules//dist/index.js +// ^^^^^ capture this +const PNPM_PATH_RE = /(['"])([^'"]*\/node_modules\/\.pnpm\/[^/]+\/node_modules\/([^/]+)\/dist\/index\.js)\1/g; + +// Match broken absolute-looking paths like 'packages/superdoc/src/types.js' +// that vite-plugin-dts sometimes emits from path alias resolution. +const BAD_ABSOLUTE_PATH_RE = /(['"])packages\/superdoc\/src\/([^'"]+)\1/g; + +// vite-plugin-dts incorrectly resolves subpath exports (e.g. @superdoc/super-editor/types) +// by appending the subpath to the main entry: '../../super-editor/src/index.js/types' +// Fix: rewrite index.js/.js +const BAD_SUBPATH_RE = /(['"])([^'"]*\/index\.js)(\/[^'"]+)\1/g; + +let fixedFiles = 0; +let totalReplacements = 0; + +const dtsFiles = findDtsFiles(distRoot); +for (const filePath of dtsFiles) { + let fileContent = fs.readFileSync(filePath, 'utf8'); + let changed = false; + + // Fix pnpm node_modules paths → bare specifiers + const pnpmResult = fileContent.replace(PNPM_PATH_RE, (match, quote, _fullPath, packageName) => { + changed = true; + totalReplacements++; + return `${quote}${packageName}${quote}`; + }); + fileContent = pnpmResult; + + // Fix broken absolute-looking paths → relative paths + const relDir = path.relative(path.dirname(filePath), path.join(distRoot, 'superdoc/src')); + fileContent = fileContent.replace(BAD_ABSOLUTE_PATH_RE, (match, quote, rest) => { + changed = true; + totalReplacements++; + const relativePath = path.posix.join( + relDir.split(path.sep).join('/'), + rest, + ); + return `${quote}${relativePath}${quote}`; + }); + + // Fix broken subpath exports (index.js/types → types.js) + fileContent = fileContent.replace(BAD_SUBPATH_RE, (match, quote, basePath, subpath) => { + changed = true; + totalReplacements++; + // Replace 'foo/index.js/types' with 'foo/types.js' + const dir = basePath.replace(/\/index\.js$/, ''); + return `${quote}${dir}${subpath}.js${quote}`; + }); + + + if (changed) { + fs.writeFileSync(filePath, fileContent); + fixedFiles++; + } +} + +if (fixedFiles > 0) { + console.log(`[ensure-types] ✓ Fixed ${totalReplacements} import paths in ${fixedFiles} .d.ts files`); +} + +// --------------------------------------------------------------------------- +// Generate ambient module declarations for private workspace packages (SD-2227) +// +// Internal .d.ts files reference @superdoc/* workspace packages that consumers +// can't install. Generate a shim so TypeScript can resolve these imports. +// Also shim prosemirror peer deps that are bundled (not in consumer node_modules). +// --------------------------------------------------------------------------- + +// Collect module specifiers and their named imports from all .d.ts files. +// Two categories: +// 1. @superdoc/* workspace packages (private, never in consumer node_modules) +// 2. Bundled/internal bare specifiers (prosemirror-*, etc.) that consumers may not install +// We generate ambient `declare module` shims for both. TypeScript uses real types if +// the package is installed, otherwise falls back to the shim. +const workspaceImports = new Map(); // module → Set +const bundledImports = new Map(); // module → Set + +// Packages that are bundled into superdoc or only used internally. +// Consumers don't need to install these — shim them for skipLibCheck:false. +const BUNDLED_PACKAGES = new Set([ + 'prosemirror-model', + 'prosemirror-state', + 'prosemirror-transform', + 'prosemirror-view', + 'eventemitter3', +]); + +// Peer deps that may not have types available for all consumers. +const PEER_PACKAGES = new Set([ + 'yjs', + '@hocuspocus/provider', + 'vue', +]); + +const SHIM_PACKAGES = new Set([...BUNDLED_PACKAGES, ...PEER_PACKAGES]); + +for (const filePath of dtsFiles) { + const fileContent = fs.readFileSync(filePath, 'utf8'); + + // Match: import { Foo, Bar } from '...' + const namedImports = fileContent.matchAll(/import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g); + for (const m of namedImports) { + const mod = m[2]; + + // Skip relative imports and already-handled packages + if (mod.startsWith('.') || mod.startsWith('@superdoc/common') || mod.startsWith('@superdoc/super-editor')) continue; + + if (mod.startsWith('@superdoc/')) { + if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set()); + const names = m[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean); + for (const name of names) workspaceImports.get(mod).add(name); + } else if (SHIM_PACKAGES.has(mod)) { + if (!bundledImports.has(mod)) bundledImports.set(mod, new Set()); + const names = m[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean); + for (const name of names) bundledImports.get(mod).add(name); + } + } + + // Match: import('...').SomeName — dynamic import type references + const dynamicImports = fileContent.matchAll(/import\(['"]([^'"]+)['"]\)\.(\w+)/g); + for (const m of dynamicImports) { + const mod = m[1]; + if (mod.startsWith('.') || mod.startsWith('@superdoc/common') || mod.startsWith('@superdoc/super-editor')) continue; + + if (mod.startsWith('@superdoc/')) { + if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set()); + workspaceImports.get(mod).add(m[2]); + } else if (SHIM_PACKAGES.has(mod)) { + if (!bundledImports.has(mod)) bundledImports.set(mod, new Set()); + bundledImports.get(mod).add(m[2]); + } + } + + // Match bare @superdoc/* module references + 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; + if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set()); + } +} + +// --------------------------------------------------------------------------- +// Write _internal-shims.d.ts +// +// Two sections: +// 1. Hand-written shims for well-known external packages (prosemirror, vue, +// yjs, eventemitter3, @hocuspocus/provider) — these need generics, class +// declarations, and namespace exports that can't be auto-generated. +// 2. Auto-generated shims for @superdoc/* workspace packages. +// --------------------------------------------------------------------------- + +const shimLines = [ + '// Auto-generated ambient declarations for internal/bundled packages.', + '// These packages are bundled into superdoc or are internal workspace packages.', + '// Consumers do not need to install them. This file prevents TypeScript errors', + '// when skipLibCheck is false. If a consumer installs the real package,', + '// TypeScript will use those types instead of these shims.', + '// NOTE: This is a script file (no exports), so `declare module` creates', + '// global ambient declarations and top-level declarations are global.', + '', + '// Buffer is used in some internal APIs but superdoc bundles the polyfill.', + '// Declare as interface (type position) + var (value position) for full compat.', + 'interface Buffer extends Uint8Array {}', + 'declare var Buffer: {', + ' from(data: any, encoding?: string): Buffer;', + ' alloc(size: number): Buffer;', + ' isBuffer(obj: any): boolean;', + '};', + '', + '// --- Well-known external packages (hand-written for correctness) ---', + '', + "declare module 'prosemirror-model' {", + ' export type DOMOutputSpec = any;', + ' export type Fragment = any;', + ' export type Mark = any;', + ' export type MarkType = any;', + ' export type Node = any;', + ' export type NodeType = any;', + ' export type ParseRule = any;', + ' export type ResolvedPos = any;', + ' export type Schema = any;', + ' export type Slice = any;', + '}', + '', + "declare module 'prosemirror-state' {", + ' export type EditorState = any;', + ' export type Plugin = any;', + ' export type PluginKey = any;', + ' export type TextSelection = any;', + ' export type Transaction = any;', + '}', + '', + "declare module 'prosemirror-transform' {", + ' export type Mapping = any;', + ' export type ReplaceAroundStep = any;', + ' export type ReplaceStep = any;', + ' export type Step = any;', + '}', + '', + "declare module 'prosemirror-view' {", + ' export type Decoration = any;', + ' export type DecorationSet = any;', + ' export type DecorationSource = any;', + ' export type EditorProps = any;', + ' export type EditorView = any;', + ' export type NodeView = any;', + '}', + '', + "declare module 'eventemitter3' {", + ' export class EventEmitter {', + ' on(event: EventTypes, fn: (...args: any[]) => void, context?: Context): this;', + ' off(event: EventTypes, fn: (...args: any[]) => void, context?: Context): this;', + ' emit(event: EventTypes, ...args: any[]): boolean;', + ' removeAllListeners(event?: EventTypes): this;', + ' }', + ' export default EventEmitter;', + '}', + '', + "declare module 'vue' {", + ' export type App = any;', + ' export type ComponentOptionsBase

= any;', + ' export type ComponentOptionsMixin = any;', + ' export type ComponentProvideOptions = any;', + ' export type ComponentPublicInstance

= any;', + ' export type ComputedRef = any;', + ' export type CreateComponentPublicInstanceWithMixins = any;', + ' export type DefineComponent

= any;', + ' export type ExtractPropTypes = any;', + ' export type GlobalComponents = any;', + ' export type GlobalDirectives = any;', + ' export type PublicProps = any;', + ' export type Ref = any;', + ' export type RendererElement = any;', + ' export type RendererNode = any;', + ' export type ShallowRef = any;', + ' export type VNode = any;', + '}', + '', + "declare module 'yjs' {", + ' export type Doc = any;', + ' export type XmlFragment = any;', + ' export type RelativePosition = any;', + '}', + '', + "declare module '@hocuspocus/provider' {", + ' export type HocuspocusProvider = any;', + '}', + '', +]; + +// --- Auto-generated @superdoc/* workspace package shims --- + +let wsCount = 0; +if (workspaceImports.size > 0) { + shimLines.push('// --- Internal workspace packages (auto-generated) ---'); + shimLines.push(''); + for (const [mod, names] of [...workspaceImports.entries()].sort((a, b) => a[0].localeCompare(b[0]))) { + wsCount++; + const sortedNames = [...names].sort(); + const exportLines = sortedNames + .filter(n => n !== 'default') + .map(n => ` export type ${n} = any;`); + if (sortedNames.includes('default')) { + exportLines.push(' const _default: any;'); + exportLines.push(' export default _default;'); + } + if (exportLines.length > 0) { + shimLines.push(`declare module '${mod}' {\n${exportLines.join('\n')}\n}`); + } else { + shimLines.push(`declare module '${mod}' { const _: any; export default _; }`); + } + } +} +shimLines.push(''); + +const shimPath = path.join(distRoot, '_internal-shims.d.ts'); +fs.writeFileSync(shimPath, shimLines.join('\n')); + +// Add reference directive to entry points so TypeScript includes the shims +const shimRef = '/// \n'; +for (const entry of requiredEntryPoints) { + const entryPath = path.join(distRoot, entry); + const entryContent = fs.readFileSync(entryPath, 'utf8'); + if (!entryContent.includes('_internal-shims.d.ts')) { + fs.writeFileSync(entryPath, shimRef + entryContent); + } +} + +const bundledCount = bundledImports.size; +console.log(`[ensure-types] ✓ Generated ambient shims for ${wsCount} workspace + ${bundledCount + 7} external modules`); + console.log('[ensure-types] ✓ Verified type entry points'); From d0a1fb9ed7a3964a3034664dd18419c68f29ce84 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 15 Mar 2026 07:26:18 -0300 Subject: [PATCH 2/6] fix(types): remove dead code and harden path rewriting in ensure-types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused bundledImports collection (symbols were collected but never used for shim generation — external shims are hand-written) - Fix relative path rewriting to prepend ./ when relDir is empty, preventing bare filenames from being treated as package specifiers - Extend import scanner regex to also match `import type { ... }` --- packages/superdoc/scripts/ensure-types.cjs | 50 ++++++---------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs index 4e425c99a4..e1d4b3742d 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -118,10 +118,14 @@ for (const filePath of dtsFiles) { fileContent = fileContent.replace(BAD_ABSOLUTE_PATH_RE, (match, quote, rest) => { changed = true; totalReplacements++; - const relativePath = path.posix.join( + let relativePath = path.posix.join( relDir.split(path.sep).join('/'), rest, ); + // Ensure relative paths start with ./ (bare names are treated as package specifiers) + if (!relativePath.startsWith('.') && !relativePath.startsWith('/')) { + relativePath = './' + relativePath; + } return `${quote}${relativePath}${quote}`; }); @@ -153,39 +157,17 @@ if (fixedFiles > 0) { // Also shim prosemirror peer deps that are bundled (not in consumer node_modules). // --------------------------------------------------------------------------- -// Collect module specifiers and their named imports from all .d.ts files. -// Two categories: -// 1. @superdoc/* workspace packages (private, never in consumer node_modules) -// 2. Bundled/internal bare specifiers (prosemirror-*, etc.) that consumers may not install -// We generate ambient `declare module` shims for both. TypeScript uses real types if -// the package is installed, otherwise falls back to the shim. +// Collect @superdoc/* workspace module specifiers and their named imports from +// all .d.ts files. These are private packages consumers can't install — we +// generate ambient `declare module` shims for them. External packages +// (prosemirror, vue, yjs, etc.) are handled by the hand-written shims below. const workspaceImports = new Map(); // module → Set -const bundledImports = new Map(); // module → Set - -// Packages that are bundled into superdoc or only used internally. -// Consumers don't need to install these — shim them for skipLibCheck:false. -const BUNDLED_PACKAGES = new Set([ - 'prosemirror-model', - 'prosemirror-state', - 'prosemirror-transform', - 'prosemirror-view', - 'eventemitter3', -]); - -// Peer deps that may not have types available for all consumers. -const PEER_PACKAGES = new Set([ - 'yjs', - '@hocuspocus/provider', - 'vue', -]); - -const SHIM_PACKAGES = new Set([...BUNDLED_PACKAGES, ...PEER_PACKAGES]); for (const filePath of dtsFiles) { const fileContent = fs.readFileSync(filePath, 'utf8'); - // Match: import { Foo, Bar } from '...' - const namedImports = fileContent.matchAll(/import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g); + // Match: import { Foo, Bar } from '...' and import type { Foo } from '...' + const namedImports = fileContent.matchAll(/import\s+(?:type\s+)?\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g); for (const m of namedImports) { const mod = m[2]; @@ -196,10 +178,6 @@ for (const filePath of dtsFiles) { if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set()); const names = m[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean); for (const name of names) workspaceImports.get(mod).add(name); - } else if (SHIM_PACKAGES.has(mod)) { - if (!bundledImports.has(mod)) bundledImports.set(mod, new Set()); - const names = m[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean); - for (const name of names) bundledImports.get(mod).add(name); } } @@ -212,9 +190,6 @@ for (const filePath of dtsFiles) { if (mod.startsWith('@superdoc/')) { if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set()); workspaceImports.get(mod).add(m[2]); - } else if (SHIM_PACKAGES.has(mod)) { - if (!bundledImports.has(mod)) bundledImports.set(mod, new Set()); - bundledImports.get(mod).add(m[2]); } } @@ -374,7 +349,6 @@ for (const entry of requiredEntryPoints) { } } -const bundledCount = bundledImports.size; -console.log(`[ensure-types] ✓ Generated ambient shims for ${wsCount} workspace + ${bundledCount + 7} external modules`); +console.log(`[ensure-types] ✓ Generated ambient shims for ${wsCount} workspace + 8 external modules`); console.log('[ensure-types] ✓ Verified type entry points'); From 9245538d7f409de9f572a55f1e009356f8a5c11f Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 15 Mar 2026 07:30:21 -0300 Subject: [PATCH 3/6] fix(types): remove Buffer shim to avoid @types/node conflicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Buffer interface shim conflicts with @types/node's generic Buffer declaration. Since most consumers have @types/node, removing the shim is safer — consumers without it get a clear TS error message suggesting `npm i --save-dev @types/node`. Also hardens PNPM path regex to handle scoped packages (@scope/pkg). --- packages/superdoc/scripts/ensure-types.cjs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs index e1d4b3742d..c2b2beb942 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -86,7 +86,7 @@ function findDtsFiles(dir) { // Captures the bare package name from the pnpm structure: // .../node_modules/.pnpm/@/node_modules//dist/index.js // ^^^^^ capture this -const PNPM_PATH_RE = /(['"])([^'"]*\/node_modules\/\.pnpm\/[^/]+\/node_modules\/([^/]+)\/dist\/index\.js)\1/g; +const PNPM_PATH_RE = /(['"])([^'"]*\/node_modules\/\.pnpm\/[^/]+\/node_modules\/(@[^/]+\/[^/]+|[^/]+)\/dist\/index\.js)\1/g; // Match broken absolute-looking paths like 'packages/superdoc/src/types.js' // that vite-plugin-dts sometimes emits from path alias resolution. @@ -221,16 +221,9 @@ const shimLines = [ '// NOTE: This is a script file (no exports), so `declare module` creates', '// global ambient declarations and top-level declarations are global.', '', - '// Buffer is used in some internal APIs but superdoc bundles the polyfill.', - '// Declare as interface (type position) + var (value position) for full compat.', - 'interface Buffer extends Uint8Array {}', - 'declare var Buffer: {', - ' from(data: any, encoding?: string): Buffer;', - ' alloc(size: number): Buffer;', - ' isBuffer(obj: any): boolean;', - '};', - '', '// --- Well-known external packages (hand-written for correctness) ---', + '// Note: Buffer is NOT shimmed here — consumers using Node.js APIs should', + '// install @types/node. Shimming Buffer conflicts with @types/node declarations.', '', "declare module 'prosemirror-model' {", ' export type DOMOutputSpec = any;', From 980306bf5a90454323b490bf147944ef666c4dcc Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 15 Mar 2026 07:43:18 -0300 Subject: [PATCH 4/6] test(types): add consumer typecheck integration test (SD-2227) Packs the built superdoc tarball into an isolated temp directory and runs tsc --noEmit with skipLibCheck: false to catch broken .d.ts imports that internal type-check doesn't detect. Added to CI pipeline after the build step. --- .github/workflows/ci-superdoc.yml | 3 ++ packages/superdoc/tests/consumer-types/run.sh | 38 +++++++++++++++++++ .../superdoc/tests/consumer-types/test.ts | 23 +++++++++++ .../tests/consumer-types/tsconfig.json | 12 ++++++ 4 files changed, 76 insertions(+) create mode 100755 packages/superdoc/tests/consumer-types/run.sh create mode 100644 packages/superdoc/tests/consumer-types/test.ts create mode 100644 packages/superdoc/tests/consumer-types/tsconfig.json diff --git a/.github/workflows/ci-superdoc.yml b/.github/workflows/ci-superdoc.yml index 70a317319d..8aae48586a 100644 --- a/.github/workflows/ci-superdoc.yml +++ b/.github/workflows/ci-superdoc.yml @@ -77,6 +77,9 @@ jobs: - name: Run slow tests run: pnpm test:slow + - name: Consumer typecheck (skipLibCheck off) + run: bash packages/superdoc/tests/consumer-types/run.sh + - name: Install Playwright for UMD smoke test run: pnpm --filter @superdoc/umd-smoke-test exec playwright install --with-deps chromium diff --git a/packages/superdoc/tests/consumer-types/run.sh b/packages/superdoc/tests/consumer-types/run.sh new file mode 100755 index 0000000000..a30c3cff03 --- /dev/null +++ b/packages/superdoc/tests/consumer-types/run.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Consumer typecheck integration test (SD-2227). +# +# Packs the built superdoc package into a tarball and type-checks a minimal +# consumer project with skipLibCheck: false. This catches broken .d.ts imports +# (pnpm paths, workspace refs, missing ambient types) that internal type-check +# doesn't detect because it runs inside the monorepo. +# +# Prerequisites: `pnpm run build` must have run first (dist/ must exist). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PKG_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +WORK_DIR="$(mktemp -d)" + +cleanup() { rm -rf "$WORK_DIR"; } +trap cleanup EXIT + +echo "==> Packing superdoc..." +TARBALL=$(cd "$PKG_DIR" && npm pack --pack-destination "$WORK_DIR" 2>/dev/null) + +echo "==> Setting up consumer project..." +cp "$SCRIPT_DIR/test.ts" "$WORK_DIR/test.ts" +cp "$SCRIPT_DIR/tsconfig.json" "$WORK_DIR/tsconfig.json" + +# Install typescript and @types/node first +npm install --prefix "$WORK_DIR" typescript @types/node --save-dev --silent 2>/dev/null + +# Extract superdoc AFTER npm install (so npm doesn't wipe it) +mkdir -p "$WORK_DIR/node_modules/superdoc" +tar xzf "$WORK_DIR/$TARBALL" -C "$WORK_DIR/node_modules/superdoc" --strip-components=1 + +echo "==> Running tsc --noEmit (skipLibCheck: false)..." +cd "$WORK_DIR" +npx tsc --noEmit + +echo "==> Consumer typecheck passed (0 errors)" diff --git a/packages/superdoc/tests/consumer-types/test.ts b/packages/superdoc/tests/consumer-types/test.ts new file mode 100644 index 0000000000..bbf568afda --- /dev/null +++ b/packages/superdoc/tests/consumer-types/test.ts @@ -0,0 +1,23 @@ +/** + * Consumer typecheck smoke test (SD-2227). + * + * This file is compiled with `tsc --noEmit` against the packed superdoc + * tarball to verify that published .d.ts files are valid for consumers + * with skipLibCheck: false. + * + * It is NOT executed at runtime — only type-checked. + */ + +// Main entry point +import type { SuperDoc } from 'superdoc'; + +// Super-editor entry point +import type { EditorView, EditorState, Transaction, Schema } from 'superdoc/super-editor'; + +// Types entry point +import type { ProseMirrorJSON, NodeConfig, MarkConfig } from 'superdoc/types'; + +// Verify the types are usable (not just importable) +type _AssertSuperDoc = SuperDoc extends object ? true : never; +type _AssertEditorView = EditorView extends object ? true : never; +type _AssertJSON = ProseMirrorJSON extends object ? true : never; diff --git a/packages/superdoc/tests/consumer-types/tsconfig.json b/packages/superdoc/tests/consumer-types/tsconfig.json new file mode 100644 index 0000000000..a1226b08eb --- /dev/null +++ b/packages/superdoc/tests/consumer-types/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "noEmit": true, + "skipLibCheck": false, + "moduleResolution": "bundler", + "module": "ESNext", + "target": "ES2020", + "types": ["node"] + }, + "include": ["test.ts"] +} From d2262c7ac91ac570763f88dbb4b6f6b0e5b08d57 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sun, 15 Mar 2026 16:33:21 -0300 Subject: [PATCH 5/6] fix(types): restore external shims with documented limitation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing prosemirror/vue/yjs shims caused 30 "Cannot find module" errors for consumers. Restored them with an honest KNOWN LIMITATION comment: ambient `declare module` overrides real types when both are present — consumers who install prosemirror alongside superdoc will see types resolve to `any`. The proper fix is adding prosemirror-* as peerDependencies (tracked separately). --- packages/superdoc/scripts/ensure-types.cjs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs index c2b2beb942..51b44e1eb8 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -206,9 +206,9 @@ for (const filePath of dtsFiles) { // Write _internal-shims.d.ts // // Two sections: -// 1. Hand-written shims for well-known external packages (prosemirror, vue, -// yjs, eventemitter3, @hocuspocus/provider) — these need generics, class -// declarations, and namespace exports that can't be auto-generated. +// 1. Hand-written shims for external packages (prosemirror-*, vue, yjs, +// eventemitter3, @hocuspocus/provider). See KNOWN LIMITATION note in the +// generated file about ambient shims overriding real package types. // 2. Auto-generated shims for @superdoc/* workspace packages. // --------------------------------------------------------------------------- @@ -216,14 +216,18 @@ const shimLines = [ '// Auto-generated ambient declarations for internal/bundled packages.', '// These packages are bundled into superdoc or are internal workspace packages.', '// Consumers do not need to install them. This file prevents TypeScript errors', - '// when skipLibCheck is false. If a consumer installs the real package,', - '// TypeScript will use those types instead of these shims.', + '// when skipLibCheck is false.', + '//', + '// KNOWN LIMITATION: ambient `declare module` with `export type X = any`', + '// overrides real package types when both are present. Consumers who install', + '// prosemirror-*, vue, yjs, or @hocuspocus/provider alongside superdoc will', + '// see those types resolve to `any`. The proper fix is adding prosemirror-*', + '// as peerDependencies so consumers always have real types available.', + '//', '// NOTE: This is a script file (no exports), so `declare module` creates', '// global ambient declarations and top-level declarations are global.', '', '// --- Well-known external packages (hand-written for correctness) ---', - '// Note: Buffer is NOT shimmed here — consumers using Node.js APIs should', - '// install @types/node. Shimming Buffer conflicts with @types/node declarations.', '', "declare module 'prosemirror-model' {", ' export type DOMOutputSpec = any;', From 9682adf1cb610ffdbbf5608b3cc262c26b105b47 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Mon, 16 Mar 2026 10:18:16 -0300 Subject: [PATCH 6/6] refactor(types): address review feedback on ensure-types and test script - Remove dead default-export branch (unreachable code) - Remove unnecessary pnpmResult intermediate variable - Replace 2>/dev/null with --quiet/--silent to preserve error diagnostics - Expand KNOWN LIMITATION comment to call out vue and eventemitter3 as guaranteed conflicts (direct deps always in consumer node_modules) --- packages/superdoc/scripts/ensure-types.cjs | 19 ++++++++----------- packages/superdoc/tests/consumer-types/run.sh | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs index 51b44e1eb8..a3f4a7f236 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -106,12 +106,11 @@ for (const filePath of dtsFiles) { let changed = false; // Fix pnpm node_modules paths → bare specifiers - const pnpmResult = fileContent.replace(PNPM_PATH_RE, (match, quote, _fullPath, packageName) => { + fileContent = fileContent.replace(PNPM_PATH_RE, (match, quote, _fullPath, packageName) => { changed = true; totalReplacements++; return `${quote}${packageName}${quote}`; }); - fileContent = pnpmResult; // Fix broken absolute-looking paths → relative paths const relDir = path.relative(path.dirname(filePath), path.join(distRoot, 'superdoc/src')); @@ -219,10 +218,13 @@ const shimLines = [ '// when skipLibCheck is false.', '//', '// KNOWN LIMITATION: ambient `declare module` with `export type X = any`', - '// overrides real package types when both are present. Consumers who install', - '// prosemirror-*, vue, yjs, or @hocuspocus/provider alongside superdoc will', - '// see those types resolve to `any`. The proper fix is adding prosemirror-*', - '// as peerDependencies so consumers always have real types available.', + '// overrides real package types when both are present. This affects:', + '// - vue, eventemitter3: direct deps of superdoc — ALWAYS in consumer', + '// node_modules, so real types are always replaced by `any`.', + '// - yjs, @hocuspocus/provider: peer deps — affected when installed.', + '// - prosemirror-*: bundled (not in consumer node_modules) — no conflict.', + '// The proper fix is adding prosemirror-* as peerDependencies and removing', + '// shims for packages consumers already have installed.', '//', '// NOTE: This is a script file (no exports), so `declare module` creates', '// global ambient declarations and top-level declarations are global.', @@ -318,12 +320,7 @@ if (workspaceImports.size > 0) { wsCount++; const sortedNames = [...names].sort(); const exportLines = sortedNames - .filter(n => n !== 'default') .map(n => ` export type ${n} = any;`); - if (sortedNames.includes('default')) { - exportLines.push(' const _default: any;'); - exportLines.push(' export default _default;'); - } if (exportLines.length > 0) { shimLines.push(`declare module '${mod}' {\n${exportLines.join('\n')}\n}`); } else { diff --git a/packages/superdoc/tests/consumer-types/run.sh b/packages/superdoc/tests/consumer-types/run.sh index a30c3cff03..05c3eda4f4 100755 --- a/packages/superdoc/tests/consumer-types/run.sh +++ b/packages/superdoc/tests/consumer-types/run.sh @@ -18,14 +18,14 @@ cleanup() { rm -rf "$WORK_DIR"; } trap cleanup EXIT echo "==> Packing superdoc..." -TARBALL=$(cd "$PKG_DIR" && npm pack --pack-destination "$WORK_DIR" 2>/dev/null) +TARBALL=$(cd "$PKG_DIR" && npm pack --pack-destination "$WORK_DIR" --quiet) echo "==> Setting up consumer project..." cp "$SCRIPT_DIR/test.ts" "$WORK_DIR/test.ts" cp "$SCRIPT_DIR/tsconfig.json" "$WORK_DIR/tsconfig.json" # Install typescript and @types/node first -npm install --prefix "$WORK_DIR" typescript @types/node --save-dev --silent 2>/dev/null +npm install --prefix "$WORK_DIR" typescript @types/node --save-dev --silent # Extract superdoc AFTER npm install (so npm doesn't wipe it) mkdir -p "$WORK_DIR/node_modules/superdoc"