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/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..a3f4a7f236 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -58,4 +58,291 @@ 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 + fileContent = fileContent.replace(PNPM_PATH_RE, (match, quote, _fullPath, packageName) => { + changed = true; + totalReplacements++; + return `${quote}${packageName}${quote}`; + }); + + // 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++; + 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}`; + }); + + // 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 @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 + +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); + 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); + } + } + + // 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]); + } + } + + // 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 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. +// --------------------------------------------------------------------------- + +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.', + '//', + '// KNOWN LIMITATION: ambient `declare module` with `export type X = any`', + '// 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.', + '', + '// --- 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 + .map(n => ` export type ${n} = any;`); + 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); + } +} + +console.log(`[ensure-types] ✓ Generated ambient shims for ${wsCount} workspace + 8 external modules`); + console.log('[ensure-types] ✓ Verified type entry points'); diff --git a/packages/superdoc/tests/consumer-types/run.sh b/packages/superdoc/tests/consumer-types/run.sh new file mode 100755 index 0000000000..05c3eda4f4 --- /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" --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 + +# 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"] +}