From e1903149dcd53494fc915808ecbe18cb0781e151 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 5 May 2026 10:37:24 -0300 Subject: [PATCH 1/2] refactor(types): remove _internal-shims.d.ts soft-landing mechanism (SD-2942) After SD-2893 drained every shim entry to zero, the auto-generated _internal-shims.d.ts file ships empty (header comments only). The auto-capture mechanism that wrote it is no longer load-bearing: it was a soft fallback that captured any unrelocated private @superdoc/* specifier in dist d.ts files and silently shimmed it as `any`. With the relocation rules + RULE1_ALLOWLIST + UNSHIMMED_PRIVATE_SPECIFIERS now covering the entire workspace surface, that soft path mostly swallows new private leaks instead of failing the build. This change makes new leaks fail loudly: - ensure-types.cjs: drop the workspace-imports scanning loops, the shim-file write, the triple-slash reference injection, and the SHIM_FORBIDDEN regression net (now redundant with the relocation rules + audit Rule 1). Add an explicit unlink for any stale _internal-shims.d.ts left over from prior builds. - audit-declarations.cjs: update the rule documentation. Rule 1 now fails for any unrelocated private @superdoc/* specifier; Rule 3 becomes a no-op in steady state (kept as defense against stale tarballs or future re-introduction). The internalShimsPresent graceful-handling already existed in audit code; no behavioral change there. A future PR that introduces a new private @superdoc/* import on the public surface fails audit Rule 1 at build time. Verified with a synthetic injection: import('@superdoc/some-new-private-pkg').T in a public-reachable d.ts produces FAIL findings: private-specifiers and exits 1. Net diff: -167 +41 lines across the two scripts. Verified: build:es clean (10 guarded packages, no shim file emitted), consumer matrix 47/0/0, runtime smoke 4/4, dist has zero _internal-shims references, negative test confirms hard-landing. --- .../superdoc/scripts/audit-declarations.cjs | 34 ++-- packages/superdoc/scripts/ensure-types.cjs | 167 +++--------------- 2 files changed, 41 insertions(+), 160 deletions(-) diff --git a/packages/superdoc/scripts/audit-declarations.cjs b/packages/superdoc/scripts/audit-declarations.cjs index 20d76e6811..163b1165ad 100644 --- a/packages/superdoc/scripts/audit-declarations.cjs +++ b/packages/superdoc/scripts/audit-declarations.cjs @@ -5,33 +5,27 @@ * and reports: * * Rule 1 (FAIL in strict mode): private workspace specifier in an emitted - * declaration that is NOT covered by `_internal-shims.d.ts` and NOT a - * legacy public surface. The shim file is the registry of "known - * unresolved" private modules whose types the RFC tolerates collapsing - * to `any`; legacy public surfaces (currently `@superdoc/super-editor`) - * resolve through the published dist tree. Anything outside that - * allowlist is a leak the RFC forbids: a consumer's strict-mode build - * fails to resolve the import. + * declaration that is NOT in `RULE1_ALLOWLIST` (legacy public surfaces, + * currently only `@superdoc/super-editor`). After SD-2942 there is no + * `_internal-shims.d.ts` fallback, so any unrelocated `@superdoc/*` + * specifier on the public surface fails the build instead of riding + * through silently as `any`. If the file is present (a stale dist from + * before SD-2942), its `declare module` entries still suppress Rule 1 + * for backward compatibility. * * Rule 2 (FAIL in strict mode): package-manager-internal paths. * `node_modules/.pnpm/...` paths leak the local install layout into a * declaration that consumers cannot resolve. * * Rule 3 (FAIL in strict mode): a relocated package reappears in - * `_internal-shims.d.ts`. The RFC's relocation pattern (SD-2842) routes - * Document API, contracts, layout-bridge, and painter-dom types through - * `superdoc`'s own dist tree; if any of those packages collapse back into - * an `any` shim, customers see the regression. This rule overlaps with - * the build-time check in `ensure-types.cjs`; keeping both lets the audit - * run as a standalone gate against any tarball, not just during a fresh - * build. + * `_internal-shims.d.ts`. With SD-2942 the file is no longer emitted + * by the build, so this rule is a no-op in steady state — kept as a + * defense if a future change re-introduces the file or runs against + * a stale tarball. * - * Informational: the set of modules still declared in `_internal-shims.d.ts`. - * The shim file may legitimately exist for legacy or internal-only - * declarations; the RFC's audit-gate rule is "no public type may resolve - * through it", not "the file must not exist". This list is reported so - * drift is visible and the surface can be tightened over time, but its - * contents do not fail the audit. + * Informational: the set of modules still declared in `_internal-shims.d.ts` + * when the file exists. After SD-2942 the file is not emitted, so this + * section is normally absent. * * Default mode is strict: findings exit non-zero so a regression cannot * ship silently. Pass `--informational` (or set diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs index 61e7bed913..a8dc704004 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -492,148 +492,35 @@ if (fs.readFileSync(superEditorFacadePath, 'utf8') !== expectedSuperEditorFacade } // --------------------------------------------------------------------------- -// 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. -// --------------------------------------------------------------------------- - -// 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. -const workspaceImports = new Map(); // module → Set - -for (const filePath of dtsFiles) { - const fileContent = fs.readFileSync(filePath, 'utf8'); - - // 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]; - - // Skip relative imports and already-handled packages - if (shouldSkipWorkspaceShim(mod)) 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 (shouldSkipWorkspaceShim(mod)) 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]; - // Skip @superdoc/super-editor (consumer-facing, not internal). All - // other @superdoc/* references (including @superdoc/common root and - // its subpaths) fall through to shim generation. The strip-and-inline - // step above handles `superdoc/src/index.d.ts`'s @superdoc/common - // import explicitly; other files importing from @superdoc/common - // resolve through the shim and collapse internal-only types - // (Comment, CommentContent, CommentJSON) to `any`. None of those - // appear on superdoc's public surface, so the collapse is safe. - if (shouldSkipWorkspaceShim(mod)) continue; - if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set()); - } -} - +// SD-2942: the auto-generated `_internal-shims.d.ts` mechanism was removed +// after SD-2893 drained every shim entry to zero. Previously this script +// scanned dist d.ts files for `from '@superdoc/...'` patterns and wrote a +// `declare module 'X' { export type Y = any; }` block for each unrelocated +// specifier — the "soft landing" path that quietly collapsed new private +// types to `any`. With SD-2893 complete, every reachable workspace type +// resolves through `RELOCATION_RULES` or stays bare for audit Rule 1 to +// reject. A future PR that introduces a new private `@superdoc/*` import +// is expected to fail the build at `audit-declarations.cjs` rather than +// ride through silently as `any`. The triple-slash reference directive +// previously injected into entry-point d.ts is also dropped; vite-plugin-dts +// emits clean entries and the next build overwrites any stale references. // --------------------------------------------------------------------------- -// Write _internal-shims.d.ts -// -// Only contains auto-generated shims for @superdoc/* workspace packages. -// External packages (prosemirror-*, vue, eventemitter3, yjs, etc.) are NOT -// shimmed — ambient `declare module` overrides real types globally, breaking -// consumers who depend on those packages (IT-852). -// --------------------------------------------------------------------------- - -const shimLines = [ - '// Auto-generated ambient declarations for internal workspace packages.', - '// These are private @superdoc/* packages that consumers cannot install.', - '// This file prevents TypeScript errors when skipLibCheck is false.', - '//', - '// External packages (prosemirror-*, vue, eventemitter3, yjs, etc.) are NOT', - '// shimmed here — their real types come from node_modules. Ambient shims for', - '// external packages would override real types globally, breaking consumers', - '// who depend on those packages (e.g. Tiptap users need real prosemirror types).', - '//', - '// NOTE: This is a script file (no exports), so `declare module` creates', - '// global ambient declarations and top-level declarations are global.', - '', -]; - -// --- 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 = []; - for (const n of sortedNames) { - // `default` is a reserved word and cannot appear in `export type - // default = any;`. When a file imports the default export of a - // private module (e.g. `import { default as Foo } from '@superdoc/common/components/Foo.vue'`), - // the named-imports collector picks up `default` as a name; emit - // a proper `export default` declaration instead. - if (n === 'default') { - exportLines.push(' const _default: any;'); - exportLines.push(' export default _default;'); - } else { - exportLines.push(` 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 modules`); - -// SD-2842 regression net: assert that no relocated package leaked back -// into the shim file. If one shows up, a future change broke the -// rewrite or include for that package and customers would see `any` -// for those types again. -const shimContent = fs.readFileSync(shimPath, 'utf8'); -const SHIM_FORBIDDEN = RELOCATION_GUARD_PACKAGES; -for (const pkg of SHIM_FORBIDDEN) { - const re = new RegExp(`declare module '${escapeRegExp(pkg)}(\\/[^']+)?'`); - if (re.test(shimContent)) { - console.error(`[ensure-types] ✗ ${pkg} appears in _internal-shims.d.ts. Its types should resolve via a relocation rewrite or fail the audit as an unrelocated leak, not via an ambient any shim. Investigate the include glob, the rewrite rule, and the shim-skip predicate for this package.`); - process.exit(1); - } +// `shouldSkipWorkspaceShim` is intentionally retained: it is no longer used +// by shim generation, but kept as documentation for the relocation policy +// (relocated specifiers + UNSHIMMED_PRIVATE_SPECIFIERS + super-editor / +// document-api legacy public surface). Future audit rules that need to +// classify workspace specifiers can reuse it. +void shouldSkipWorkspaceShim; + +// Clean up artifacts from the old shim mechanism. vite-plugin-dts overwrites +// entry-point d.ts on each build, so the triple-slash references injected by +// the old code are wiped automatically; only the shim file itself persists +// across builds and needs an explicit unlink. +const legacyShimPath = path.join(distRoot, '_internal-shims.d.ts'); +if (fs.existsSync(legacyShimPath)) { + fs.unlinkSync(legacyShimPath); + console.log('[ensure-types] ✓ Removed legacy _internal-shims.d.ts'); } -console.log(`[ensure-types] ✓ Verified ${SHIM_FORBIDDEN.length} relocated packages do not appear in shim file`); console.log('[ensure-types] ✓ Verified type entry points'); From c3a1a95007fde63c4b8ed725d1d60341f6b5bea1 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 5 May 2026 11:32:27 -0300 Subject: [PATCH 2/2] docs(types): fix stale comment about @superdoc/common shim path (SD-2942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment above the inline-replacement block was inherited from the pre-SD-2893 era and described two things that are no longer true after the shim drain: 1. "fall through to the ambient shim block below" — SD-2942 (this PR) removes the shim block, so non-main-entry @superdoc/common imports now resolve via the RELOCATION_RULES rewriter, not via a fallback shim. 2. "Comment, CommentContent, CommentJSON ... not on the public surface" — SD-2893 stack 6 (PR #3154) relocated these types via the bare @superdoc/common rule mapping to comments-types.d.ts. `Comment` is now publicly importable as `import type { Comment } from 'superdoc/super-editor'`. Replace the block with a description of what the inline-replacement step actually does today: handle the main entry's runtime-value imports (DOCX, PDF, HTML, getFileObject, compareVersions, BlankDOCX) which are not type-only and so the relocation rule cannot serve them. --- packages/superdoc/scripts/ensure-types.cjs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs index a8dc704004..8ee0ed423c 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -123,13 +123,15 @@ if (!hasSuperDocExport) { process.exit(1); } -// Fix workspace package imports that aren't resolvable by consumers. -// @superdoc/common is a private workspace package — inline its types in -// the main entry. Other reachable d.ts files that import from -// @superdoc/common fall through to the ambient shim block below; those -// imports surface internal types (Comment, CommentContent, CommentJSON) -// that are not on the public surface, so collapsing them to `any` via -// the shim is correct. +// @superdoc/common is a private workspace package, so consumers can't +// resolve a bare `from '@superdoc/common'` import. The main entry +// (superdoc/src/index.d.ts) imports runtime values from it — DOCX/PDF/ +// HTML constants, getFileObject, compareVersions, BlankDOCX (the last +// from a Vite `?url` import that vite-plugin-dts can't type). Strip +// that import statement and inline ambient declarations for those +// values. Type-only imports of @superdoc/common from other dist files +// are handled separately by the RELOCATION_RULES rewriter below, which +// maps bare @superdoc/common to dist/shared/common/comments-types.d.ts. const hadWorkspaceImport = content.includes('@superdoc/common'); if (hadWorkspaceImport) { // Replace the @superdoc/common import with inline declarations