Skip to content

Commit 77807e5

Browse files
authored
fix(types): fix broken .d.ts imports in published superdoc package (SD-2227) (#2392)
* fix(types): fix broken .d.ts imports in published superdoc package (SD-2227) 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. * fix(types): remove dead code and harden path rewriting in ensure-types - 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 { ... }` * fix(types): remove Buffer shim to avoid @types/node conflicts The Buffer interface shim conflicts with @types/node's generic Buffer<TArrayBuffer> 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). * 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. * fix(types): restore external shims with documented limitation 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). * 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)
1 parent 58ffe42 commit 77807e5

6 files changed

Lines changed: 368 additions & 3 deletions

File tree

.github/workflows/ci-superdoc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ jobs:
7777
- name: Run slow tests
7878
run: pnpm test:slow
7979

80+
- name: Consumer typecheck (skipLibCheck off)
81+
run: bash packages/superdoc/tests/consumer-types/run.sh
82+
8083
- name: Install Playwright for UMD smoke test
8184
run: pnpm --filter @superdoc/umd-smoke-test exec playwright install --with-deps chromium
8285

packages/super-editor/src/index.d.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
* This file provides TypeScript types for the JavaScript exports in index.js
44
*/
55

6-
export type { EditorView } from 'prosemirror-view';
7-
export type { EditorState, Transaction } from 'prosemirror-state';
8-
export type { Schema } from 'prosemirror-model';
6+
// Re-export prosemirror types for consumers AND import for local use
7+
import type { EditorView } from 'prosemirror-view';
8+
import type { EditorState, Transaction } from 'prosemirror-state';
9+
import type { Schema } from 'prosemirror-model';
10+
export type { EditorView, EditorState, Transaction, Schema };
911

1012
// ============================================
1113
// COMMAND TYPES (inlined from ChainedCommands.ts)

packages/superdoc/scripts/ensure-types.cjs

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,291 @@ if (hadWorkspaceImport) {
5858
console.log('[ensure-types] ✓ Inlined @superdoc/common types');
5959
}
6060

61+
// ---------------------------------------------------------------------------
62+
// Fix pnpm node_modules paths in ALL .d.ts files (SD-2227)
63+
//
64+
// vite-plugin-dts resolves bare specifiers like 'prosemirror-view' to physical
65+
// pnpm paths like '../../node_modules/.pnpm/prosemirror-view@1.41.5/node_modules/prosemirror-view/dist/index.js'.
66+
// Consumers don't have these paths — rewrite them back to bare specifiers.
67+
// ---------------------------------------------------------------------------
68+
69+
/**
70+
* Recursively find all .d.ts files under a directory.
71+
*/
72+
function findDtsFiles(dir) {
73+
const results = [];
74+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
75+
const fullPath = path.join(dir, entry.name);
76+
if (entry.isDirectory()) {
77+
results.push(...findDtsFiles(fullPath));
78+
} else if (entry.name.endsWith('.d.ts')) {
79+
results.push(fullPath);
80+
}
81+
}
82+
return results;
83+
}
84+
85+
// Match pnpm node_modules paths in both `from '...'` and `import('...')` contexts.
86+
// Captures the bare package name from the pnpm structure:
87+
// .../node_modules/.pnpm/<pkg>@<ver>/node_modules/<pkg>/dist/index.js
88+
// ^^^^^ capture this
89+
const PNPM_PATH_RE = /(['"])([^'"]*\/node_modules\/\.pnpm\/[^/]+\/node_modules\/(@[^/]+\/[^/]+|[^/]+)\/dist\/index\.js)\1/g;
90+
91+
// Match broken absolute-looking paths like 'packages/superdoc/src/types.js'
92+
// that vite-plugin-dts sometimes emits from path alias resolution.
93+
const BAD_ABSOLUTE_PATH_RE = /(['"])packages\/superdoc\/src\/([^'"]+)\1/g;
94+
95+
// vite-plugin-dts incorrectly resolves subpath exports (e.g. @superdoc/super-editor/types)
96+
// by appending the subpath to the main entry: '../../super-editor/src/index.js/types'
97+
// Fix: rewrite index.js/<subpath> → <subpath>.js
98+
const BAD_SUBPATH_RE = /(['"])([^'"]*\/index\.js)(\/[^'"]+)\1/g;
99+
100+
let fixedFiles = 0;
101+
let totalReplacements = 0;
102+
103+
const dtsFiles = findDtsFiles(distRoot);
104+
for (const filePath of dtsFiles) {
105+
let fileContent = fs.readFileSync(filePath, 'utf8');
106+
let changed = false;
107+
108+
// Fix pnpm node_modules paths → bare specifiers
109+
fileContent = fileContent.replace(PNPM_PATH_RE, (match, quote, _fullPath, packageName) => {
110+
changed = true;
111+
totalReplacements++;
112+
return `${quote}${packageName}${quote}`;
113+
});
114+
115+
// Fix broken absolute-looking paths → relative paths
116+
const relDir = path.relative(path.dirname(filePath), path.join(distRoot, 'superdoc/src'));
117+
fileContent = fileContent.replace(BAD_ABSOLUTE_PATH_RE, (match, quote, rest) => {
118+
changed = true;
119+
totalReplacements++;
120+
let relativePath = path.posix.join(
121+
relDir.split(path.sep).join('/'),
122+
rest,
123+
);
124+
// Ensure relative paths start with ./ (bare names are treated as package specifiers)
125+
if (!relativePath.startsWith('.') && !relativePath.startsWith('/')) {
126+
relativePath = './' + relativePath;
127+
}
128+
return `${quote}${relativePath}${quote}`;
129+
});
130+
131+
// Fix broken subpath exports (index.js/types → types.js)
132+
fileContent = fileContent.replace(BAD_SUBPATH_RE, (match, quote, basePath, subpath) => {
133+
changed = true;
134+
totalReplacements++;
135+
// Replace 'foo/index.js/types' with 'foo/types.js'
136+
const dir = basePath.replace(/\/index\.js$/, '');
137+
return `${quote}${dir}${subpath}.js${quote}`;
138+
});
139+
140+
141+
if (changed) {
142+
fs.writeFileSync(filePath, fileContent);
143+
fixedFiles++;
144+
}
145+
}
146+
147+
if (fixedFiles > 0) {
148+
console.log(`[ensure-types] ✓ Fixed ${totalReplacements} import paths in ${fixedFiles} .d.ts files`);
149+
}
150+
151+
// ---------------------------------------------------------------------------
152+
// Generate ambient module declarations for private workspace packages (SD-2227)
153+
//
154+
// Internal .d.ts files reference @superdoc/* workspace packages that consumers
155+
// can't install. Generate a shim so TypeScript can resolve these imports.
156+
// Also shim prosemirror peer deps that are bundled (not in consumer node_modules).
157+
// ---------------------------------------------------------------------------
158+
159+
// Collect @superdoc/* workspace module specifiers and their named imports from
160+
// all .d.ts files. These are private packages consumers can't install — we
161+
// generate ambient `declare module` shims for them. External packages
162+
// (prosemirror, vue, yjs, etc.) are handled by the hand-written shims below.
163+
const workspaceImports = new Map(); // module → Set<name>
164+
165+
for (const filePath of dtsFiles) {
166+
const fileContent = fs.readFileSync(filePath, 'utf8');
167+
168+
// Match: import { Foo, Bar } from '...' and import type { Foo } from '...'
169+
const namedImports = fileContent.matchAll(/import\s+(?:type\s+)?\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g);
170+
for (const m of namedImports) {
171+
const mod = m[2];
172+
173+
// Skip relative imports and already-handled packages
174+
if (mod.startsWith('.') || mod.startsWith('@superdoc/common') || mod.startsWith('@superdoc/super-editor')) continue;
175+
176+
if (mod.startsWith('@superdoc/')) {
177+
if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set());
178+
const names = m[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
179+
for (const name of names) workspaceImports.get(mod).add(name);
180+
}
181+
}
182+
183+
// Match: import('...').SomeName — dynamic import type references
184+
const dynamicImports = fileContent.matchAll(/import\(['"]([^'"]+)['"]\)\.(\w+)/g);
185+
for (const m of dynamicImports) {
186+
const mod = m[1];
187+
if (mod.startsWith('.') || mod.startsWith('@superdoc/common') || mod.startsWith('@superdoc/super-editor')) continue;
188+
189+
if (mod.startsWith('@superdoc/')) {
190+
if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set());
191+
workspaceImports.get(mod).add(m[2]);
192+
}
193+
}
194+
195+
// Match bare @superdoc/* module references
196+
const bareRefs = fileContent.matchAll(/['"](@superdoc\/[^'"]+)['"]/g);
197+
for (const m of bareRefs) {
198+
const mod = m[1];
199+
if (mod.startsWith('@superdoc/common') || mod.startsWith('@superdoc/super-editor')) continue;
200+
if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set());
201+
}
202+
}
203+
204+
// ---------------------------------------------------------------------------
205+
// Write _internal-shims.d.ts
206+
//
207+
// Two sections:
208+
// 1. Hand-written shims for external packages (prosemirror-*, vue, yjs,
209+
// eventemitter3, @hocuspocus/provider). See KNOWN LIMITATION note in the
210+
// generated file about ambient shims overriding real package types.
211+
// 2. Auto-generated shims for @superdoc/* workspace packages.
212+
// ---------------------------------------------------------------------------
213+
214+
const shimLines = [
215+
'// Auto-generated ambient declarations for internal/bundled packages.',
216+
'// These packages are bundled into superdoc or are internal workspace packages.',
217+
'// Consumers do not need to install them. This file prevents TypeScript errors',
218+
'// when skipLibCheck is false.',
219+
'//',
220+
'// KNOWN LIMITATION: ambient `declare module` with `export type X = any`',
221+
'// overrides real package types when both are present. This affects:',
222+
'// - vue, eventemitter3: direct deps of superdoc — ALWAYS in consumer',
223+
'// node_modules, so real types are always replaced by `any`.',
224+
'// - yjs, @hocuspocus/provider: peer deps — affected when installed.',
225+
'// - prosemirror-*: bundled (not in consumer node_modules) — no conflict.',
226+
'// The proper fix is adding prosemirror-* as peerDependencies and removing',
227+
'// shims for packages consumers already have installed.',
228+
'//',
229+
'// NOTE: This is a script file (no exports), so `declare module` creates',
230+
'// global ambient declarations and top-level declarations are global.',
231+
'',
232+
'// --- Well-known external packages (hand-written for correctness) ---',
233+
'',
234+
"declare module 'prosemirror-model' {",
235+
' export type DOMOutputSpec = any;',
236+
' export type Fragment = any;',
237+
' export type Mark = any;',
238+
' export type MarkType = any;',
239+
' export type Node = any;',
240+
' export type NodeType = any;',
241+
' export type ParseRule = any;',
242+
' export type ResolvedPos = any;',
243+
' export type Schema<N extends string = any, M extends string = any> = any;',
244+
' export type Slice = any;',
245+
'}',
246+
'',
247+
"declare module 'prosemirror-state' {",
248+
' export type EditorState = any;',
249+
' export type Plugin<T = any> = any;',
250+
' export type PluginKey<T = any> = any;',
251+
' export type TextSelection = any;',
252+
' export type Transaction = any;',
253+
'}',
254+
'',
255+
"declare module 'prosemirror-transform' {",
256+
' export type Mapping = any;',
257+
' export type ReplaceAroundStep = any;',
258+
' export type ReplaceStep = any;',
259+
' export type Step = any;',
260+
'}',
261+
'',
262+
"declare module 'prosemirror-view' {",
263+
' export type Decoration = any;',
264+
' export type DecorationSet = any;',
265+
' export type DecorationSource = any;',
266+
' export type EditorProps = any;',
267+
' export type EditorView = any;',
268+
' export type NodeView = any;',
269+
'}',
270+
'',
271+
"declare module 'eventemitter3' {",
272+
' export class EventEmitter<EventTypes extends string | symbol = string | symbol, Context = any> {',
273+
' on(event: EventTypes, fn: (...args: any[]) => void, context?: Context): this;',
274+
' off(event: EventTypes, fn: (...args: any[]) => void, context?: Context): this;',
275+
' emit(event: EventTypes, ...args: any[]): boolean;',
276+
' removeAllListeners(event?: EventTypes): this;',
277+
' }',
278+
' export default EventEmitter;',
279+
'}',
280+
'',
281+
"declare module 'vue' {",
282+
' export type App<T = any> = any;',
283+
' export type ComponentOptionsBase<P = any, B = any, D = any, C = any, M = any, Mixin = any, Extends = any, E = any, EE = any, Defaults = any, I = any, II = any, S = any, LC = any, Directives = any, Exposed = any, Provide = any> = any;',
284+
' export type ComponentOptionsMixin = any;',
285+
' export type ComponentProvideOptions = any;',
286+
' export type ComponentPublicInstance<P = any, B = any, D = any, C = any, M = any, E = any, S = any, Options = any, Defaults = any, MakeDefaultsOptional = any, I = any, PublicMixin = any, A = any, B2 = any, C2 = any> = any;',
287+
' export type ComputedRef<T = any> = any;',
288+
' export type CreateComponentPublicInstanceWithMixins<T = any, S = any, U = any, V = any, W = any, X = any, Y = any, Z = any, A = any, B = any, C = any, D = any> = any;',
289+
' export type DefineComponent<P = any, B = any, D = any, C = any, M = any, Mixin = any, Extends = any, E = any, EE = any, PP = any, Props = any, Defaults = any, S = any> = any;',
290+
' export type ExtractPropTypes<T = any> = any;',
291+
' export type GlobalComponents = any;',
292+
' export type GlobalDirectives = any;',
293+
' export type PublicProps = any;',
294+
' export type Ref<T = any> = any;',
295+
' export type RendererElement = any;',
296+
' export type RendererNode = any;',
297+
' export type ShallowRef<T = any> = any;',
298+
' export type VNode = any;',
299+
'}',
300+
'',
301+
"declare module 'yjs' {",
302+
' export type Doc = any;',
303+
' export type XmlFragment = any;',
304+
' export type RelativePosition = any;',
305+
'}',
306+
'',
307+
"declare module '@hocuspocus/provider' {",
308+
' export type HocuspocusProvider = any;',
309+
'}',
310+
'',
311+
];
312+
313+
// --- Auto-generated @superdoc/* workspace package shims ---
314+
315+
let wsCount = 0;
316+
if (workspaceImports.size > 0) {
317+
shimLines.push('// --- Internal workspace packages (auto-generated) ---');
318+
shimLines.push('');
319+
for (const [mod, names] of [...workspaceImports.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
320+
wsCount++;
321+
const sortedNames = [...names].sort();
322+
const exportLines = sortedNames
323+
.map(n => ` export type ${n} = any;`);
324+
if (exportLines.length > 0) {
325+
shimLines.push(`declare module '${mod}' {\n${exportLines.join('\n')}\n}`);
326+
} else {
327+
shimLines.push(`declare module '${mod}' { const _: any; export default _; }`);
328+
}
329+
}
330+
}
331+
shimLines.push('');
332+
333+
const shimPath = path.join(distRoot, '_internal-shims.d.ts');
334+
fs.writeFileSync(shimPath, shimLines.join('\n'));
335+
336+
// Add reference directive to entry points so TypeScript includes the shims
337+
const shimRef = '/// <reference path="../../_internal-shims.d.ts" />\n';
338+
for (const entry of requiredEntryPoints) {
339+
const entryPath = path.join(distRoot, entry);
340+
const entryContent = fs.readFileSync(entryPath, 'utf8');
341+
if (!entryContent.includes('_internal-shims.d.ts')) {
342+
fs.writeFileSync(entryPath, shimRef + entryContent);
343+
}
344+
}
345+
346+
console.log(`[ensure-types] ✓ Generated ambient shims for ${wsCount} workspace + 8 external modules`);
347+
61348
console.log('[ensure-types] ✓ Verified type entry points');
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env bash
2+
# Consumer typecheck integration test (SD-2227).
3+
#
4+
# Packs the built superdoc package into a tarball and type-checks a minimal
5+
# consumer project with skipLibCheck: false. This catches broken .d.ts imports
6+
# (pnpm paths, workspace refs, missing ambient types) that internal type-check
7+
# doesn't detect because it runs inside the monorepo.
8+
#
9+
# Prerequisites: `pnpm run build` must have run first (dist/ must exist).
10+
11+
set -euo pipefail
12+
13+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14+
PKG_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
15+
WORK_DIR="$(mktemp -d)"
16+
17+
cleanup() { rm -rf "$WORK_DIR"; }
18+
trap cleanup EXIT
19+
20+
echo "==> Packing superdoc..."
21+
TARBALL=$(cd "$PKG_DIR" && npm pack --pack-destination "$WORK_DIR" --quiet)
22+
23+
echo "==> Setting up consumer project..."
24+
cp "$SCRIPT_DIR/test.ts" "$WORK_DIR/test.ts"
25+
cp "$SCRIPT_DIR/tsconfig.json" "$WORK_DIR/tsconfig.json"
26+
27+
# Install typescript and @types/node first
28+
npm install --prefix "$WORK_DIR" typescript @types/node --save-dev --silent
29+
30+
# Extract superdoc AFTER npm install (so npm doesn't wipe it)
31+
mkdir -p "$WORK_DIR/node_modules/superdoc"
32+
tar xzf "$WORK_DIR/$TARBALL" -C "$WORK_DIR/node_modules/superdoc" --strip-components=1
33+
34+
echo "==> Running tsc --noEmit (skipLibCheck: false)..."
35+
cd "$WORK_DIR"
36+
npx tsc --noEmit
37+
38+
echo "==> Consumer typecheck passed (0 errors)"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Consumer typecheck smoke test (SD-2227).
3+
*
4+
* This file is compiled with `tsc --noEmit` against the packed superdoc
5+
* tarball to verify that published .d.ts files are valid for consumers
6+
* with skipLibCheck: false.
7+
*
8+
* It is NOT executed at runtime — only type-checked.
9+
*/
10+
11+
// Main entry point
12+
import type { SuperDoc } from 'superdoc';
13+
14+
// Super-editor entry point
15+
import type { EditorView, EditorState, Transaction, Schema } from 'superdoc/super-editor';
16+
17+
// Types entry point
18+
import type { ProseMirrorJSON, NodeConfig, MarkConfig } from 'superdoc/types';
19+
20+
// Verify the types are usable (not just importable)
21+
type _AssertSuperDoc = SuperDoc extends object ? true : never;
22+
type _AssertEditorView = EditorView extends object ? true : never;
23+
type _AssertJSON = ProseMirrorJSON extends object ? true : never;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"noEmit": true,
5+
"skipLibCheck": false,
6+
"moduleResolution": "bundler",
7+
"module": "ESNext",
8+
"target": "ES2020",
9+
"types": ["node"]
10+
},
11+
"include": ["test.ts"]
12+
}

0 commit comments

Comments
 (0)