From e1903149dcd53494fc915808ecbe18cb0781e151 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 5 May 2026 10:37:24 -0300 Subject: [PATCH 1/6] 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/6] 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 From 8b84be7b330ea5557fcf3c42ca6542ea2d4d2f3c Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 5 May 2026 12:08:25 -0300 Subject: [PATCH 3/6] refactor(types): centralize public-surface taxonomy in single config (SD-2864) The same workspace-relocation taxonomy was duplicated across four files: ensure-types.cjs, audit-declarations.cjs, vite.config.js, and tsconfig.json. Adding a new public-surface relocation required coordinated edits in all four. PR #3144 (pm-adapter) and several SD-2893 stack PRs each shipped a regression caused by drift between these lists. Add packages/superdoc/scripts/type-surface.config.cjs as the single source of truth and refactor each consumer to derive what it needs. Consumer changes: - ensure-types.cjs: derives RELOCATION_RULES, RELOCATION_GUARD_PACKAGES, UNSHIMMED_PRIVATE_SPECIFIERS, SHARED_COMMON_DTS_TARGETS, requiredEntryPoints, and HANDWRITTEN_DTS_BLOCKLIST from the config. - audit-declarations.cjs: derives RELOCATION_GUARD_PACKAGES and RULE1_ALLOWLIST from the config. - vite.config.js: derives the dts include list by spreading relocations[*].viteIncludes after the foundational base entries (uses createRequire to load the CJS config from ESM Vite config). - tsconfig.json stays hand-edited (it's plain JSON), but a new check-tsconfig-type-surface.cjs script enforces parity by verifying every relocation's tsconfigIncludes paths are present in tsconfig.json's include array. Wired into postbuild so drift fails the build. Net diff: -138 +47 lines across the consumer scripts plus +208 lines for the new config + parity check. The config is the authoritative taxonomy; the parity check is the safety net that makes accidental drift loud. Verified: build:es clean (10 guarded packages, no shim file emitted), consumer matrix 47/0/0, declaration audit clean. Negative drift test: removing pm-adapter entries from tsconfig.json's include makes the parity check exit 1 with a clear message naming the missing entries and the relocation that requires them; restoring exits 0. --- packages/superdoc/package.json | 2 +- .../superdoc/scripts/audit-declarations.cjs | 39 +-- .../scripts/check-tsconfig-type-surface.cjs | 43 ++++ packages/superdoc/scripts/ensure-types.cjs | 103 ++------ .../superdoc/scripts/type-surface.config.cjs | 222 ++++++++++++++++++ packages/superdoc/vite.config.js | 41 +--- 6 files changed, 312 insertions(+), 138 deletions(-) create mode 100644 packages/superdoc/scripts/check-tsconfig-type-surface.cjs create mode 100644 packages/superdoc/scripts/type-surface.config.cjs diff --git a/packages/superdoc/package.json b/packages/superdoc/package.json index 0e5df28394..ff3e0c2f2c 100644 --- a/packages/superdoc/package.json +++ b/packages/superdoc/package.json @@ -109,7 +109,7 @@ "word-benchmark-sidecar": "node ../../devtools/word-benchmark-sidecar/server.js", "build": "vite build && pnpm run build:cdn", "build:dev": "SUPERDOC_SKIP_DTS=1 vite build", - "postbuild": "node ./scripts/ensure-types.cjs && node ./scripts/audit-bundle.cjs && node ./scripts/audit-declarations.cjs", + "postbuild": "node ./scripts/check-tsconfig-type-surface.cjs && node ./scripts/ensure-types.cjs && node ./scripts/audit-bundle.cjs && node ./scripts/audit-declarations.cjs", "audit:declarations": "node ./scripts/audit-declarations.cjs", "audit:declarations:informational": "node ./scripts/audit-declarations.cjs --informational", "check:jsdoc": "node ./scripts/check-jsdoc.cjs", diff --git a/packages/superdoc/scripts/audit-declarations.cjs b/packages/superdoc/scripts/audit-declarations.cjs index 163b1165ad..7552024b5a 100644 --- a/packages/superdoc/scripts/audit-declarations.cjs +++ b/packages/superdoc/scripts/audit-declarations.cjs @@ -36,6 +36,11 @@ const fs = require('node:fs'); const path = require('node:path'); +// SD-2864: canonical taxonomy for the published type surface. The lists +// below previously duplicated data from ensure-types.cjs; both now derive +// from the same config so the two scripts cannot drift. +const typeSurface = require('./type-surface.config.cjs'); + const distRoot = path.resolve(__dirname, '..', 'dist'); // SD-2859: strict is the default. The audit fails the build on any @@ -52,35 +57,17 @@ if (!fs.existsSync(distRoot)) { process.exit(1); } -// Packages whose public type dependencies are relocated into `superdoc`'s -// published declaration tree or explicitly guarded from falling back to an -// ambient shim. They must NEVER appear as a `declare module` block in -// `_internal-shims.d.ts` — if they do, their types collapse to `any` for -// consumers and we have a regression. Mirror of SD-2842's -// `RELOCATION_GUARD_PACKAGES` in `ensure-types.cjs`; keep the two lists in sync. -const RELOCATION_GUARD_PACKAGES = [ - '@superdoc/document-api', - '@superdoc/contracts', - '@superdoc/dom-contract', - '@superdoc/layout-bridge', - '@superdoc/layout-engine', - '@superdoc/painter-dom', - '@superdoc/pm-adapter', - '@superdoc/style-engine', - '@superdoc/common', - '@superdoc/common/list-marker-utils', -]; +// Packages that must NEVER appear as a `declare module` block in +// `_internal-shims.d.ts`. After SD-2942 the file is no longer emitted, so +// this list is a defense against stale tarballs and future re-introduction. +// Source: type-surface.config.cjs `relocationGuardPackages`. +const RELOCATION_GUARD_PACKAGES = typeSurface.relocationGuardPackages; // Specifiers that may appear as bare imports in published d.ts files even // though they are private workspace packages. Each entry has a documented -// reason; anything outside this allowlist (and outside the shim file) is a -// real leak per Rule 1. -const RULE1_ALLOWLIST = { - // Legacy public surface per the RFC. Resolves through `superdoc`'s - // published dist tree at runtime via the existing rewrite/include rules. - // Deep subpaths beyond the curated public surface are NOT allowlisted. - '@superdoc/super-editor': 'legacy public surface (RFC Decision 1)', -}; +// reason; anything outside this allowlist is a real leak per Rule 1. +// Source: type-surface.config.cjs `rule1Allowlist`. +const RULE1_ALLOWLIST = typeSurface.rule1Allowlist; function isRule1Allowed(specifier, shimmedSet) { if (RULE1_ALLOWLIST[specifier]) return true; diff --git a/packages/superdoc/scripts/check-tsconfig-type-surface.cjs b/packages/superdoc/scripts/check-tsconfig-type-surface.cjs new file mode 100644 index 0000000000..5833807a96 --- /dev/null +++ b/packages/superdoc/scripts/check-tsconfig-type-surface.cjs @@ -0,0 +1,43 @@ +#!/usr/bin/env node +/** + * SD-2864: enforce parity between tsconfig.json's `include` array and the + * relocation entries in `type-surface.config.cjs`. tsconfig.json is the + * one consumer of the type-surface taxonomy that has no scripting layer + * (it's plain JSON), so we don't generate it; instead this check fails + * the build if the on-disk file drifts from the config. + * + * Allowed shape: tsconfig.json's `include` MUST contain every entry in + * each relocation's `tsconfigIncludes`. Additional entries (the + * foundational `src`, `../super-editor/src`, `../document-api/src`, + * etc.) are tolerated - we only enforce the relocation taxonomy. + * + * Drift modes this catches: + * - A new relocation added to the config but not mirrored in tsconfig.json + * (the typecheck for that source tree would silently miss it). + * - A relocation removed from the config but its tsconfig.json entry left + * stale (less severe but still drift). + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const tsconfigPath = path.resolve(__dirname, '..', 'tsconfig.json'); +const typeSurface = require('./type-surface.config.cjs'); + +const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf8')); +const tsconfigIncludes = new Set(tsconfig.include || []); + +const expected = typeSurface.relocations.flatMap((r) => r.tsconfigIncludes); +const missing = expected.filter((entry) => !tsconfigIncludes.has(entry)); + +if (missing.length > 0) { + console.error('[check-tsconfig-type-surface] tsconfig.json `include` is missing relocation entries:'); + for (const entry of missing) { + const owner = typeSurface.relocations.find((r) => r.tsconfigIncludes.includes(entry)); + console.error(` - ${entry} (required by ${owner.pkg})`); + } + console.error('Add the entries above to packages/superdoc/tsconfig.json or update type-surface.config.cjs.'); + process.exit(1); +} + +console.log(`[check-tsconfig-type-surface] ✓ tsconfig.json mirrors ${expected.length} relocation include paths`); diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs index 8ee0ed423c..507ba0d949 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -3,6 +3,11 @@ const fs = require('node:fs'); const path = require('node:path'); +// SD-2864: canonical taxonomy for the published type surface. Mirrors +// vite.config.js, tsconfig.json, and audit-declarations.cjs from a single +// data file so contributors only edit one place to add a new relocation. +const typeSurface = require('./type-surface.config.cjs'); + // Verify that vite-plugin-dts generated the expected type entry points. // Path aliases are resolved by vite-plugin-dts via tsconfig.json paths. const distRoot = path.resolve(__dirname, '..', 'dist'); @@ -243,84 +248,20 @@ function rewriteDocApiPaths(fileContent, filePath) { }); } -// SD-2842: relocate workspace packages whose types appear on the -// public surface. Same idea as the document-api rewrite above: emit -// their declarations into superdoc's dist (via vite-plugin-dts include) -// and redirect bare specifiers in emitted .d.ts files to relative -// paths the consumer can resolve. -// -// SD-2893 note for pm-adapter: only specific type subpaths are -// relocated (see vite.config.js include list). Do not add a broad -// `@superdoc/pm-adapter` rule unless the barrel declaration is also -// emitted; otherwise a bare specifier would rewrite to a missing -// relative path and evade the audit gate. -const RELOCATION_RULES = [ - { pkg: '@superdoc/contracts', distEntry: 'layout-engine/contracts/src/index.d.ts', matchSubpaths: true }, - { pkg: '@superdoc/dom-contract', distEntry: 'layout-engine/dom-contract/src/index.d.ts', matchSubpaths: true }, - { pkg: '@superdoc/layout-bridge', distEntry: 'layout-engine/layout-bridge/src/index.d.ts', matchSubpaths: true }, - { pkg: '@superdoc/layout-engine', distEntry: 'layout-engine/layout-engine/src/index.d.ts', matchSubpaths: true }, - { pkg: '@superdoc/painter-dom', distEntry: 'layout-engine/painters/dom/src/index.d.ts', matchSubpaths: true }, - { - pkg: '@superdoc/pm-adapter/converter-context.js', - distEntry: 'layout-engine/pm-adapter/src/converter-context.d.ts', - matchSubpaths: false, - }, - { - pkg: '@superdoc/pm-adapter/sections/types.js', - distEntry: 'layout-engine/pm-adapter/src/sections/types.d.ts', - matchSubpaths: false, - }, - // SD-2893: list-marker-utils is the only @superdoc/common subpath publicly - // reachable today (via painter-dom). Relocate just this file so the bare - // @superdoc/common shim does not capture it; the parent @superdoc/common - // package and other subpaths stay shimmed until separately drained. - { - pkg: '@superdoc/common/list-marker-utils', - distEntry: 'shared/common/list-marker-utils.d.ts', - matchSubpaths: false, - }, - // SD-2893: only the /ooxml subpath of style-engine is publicly reachable. - // Relocate just this subpath plus its sibling cascade.ts dependency - // (see vite.config.js include list). The bare @superdoc/style-engine is - // guarded but unrewritten; if a future bare-barrel leak appears the audit - // gate fails rather than producing a missing relative path. - { - pkg: '@superdoc/style-engine/ooxml', - distEntry: 'layout-engine/style-engine/src/ooxml/index.d.ts', - matchSubpaths: false, - }, - // SD-2893: bare @superdoc/common appears in three internal-only dist d.ts - // files for the four Comment* types (Comment, CommentContent, CommentJSON, - // CommentThreadingProfile). Point the bare specifier at comments-types.d.ts - // (emitted via SHARED_COMMON_DTS_TARGETS) so the rewrite resolves to a real - // file. matchSubpaths: false because only the bare specifier is referenced; - // any future @superdoc/common/ import would not be auto- - // rewritten, falling through to the audit gate. The runtime-value imports - // from the main entry (DOCX, PDF, HTML, getFileObject, compareVersions, - // BlankDOCX) are still handled by the inline-replacement step above. - { - pkg: '@superdoc/common', - distEntry: 'shared/common/comments-types.d.ts', - matchSubpaths: false, - }, -]; +// SD-2842 / SD-2864: relocate workspace packages whose types appear on the +// public surface. Each rule redirects bare/subpath specifiers in emitted +// .d.ts files to a relative path inside dist. The canonical list lives in +// type-surface.config.cjs; this script picks the fields it needs. +const RELOCATION_RULES = typeSurface.relocations.map(({ pkg, distEntry, matchSubpaths }) => ({ + pkg, + distEntry, + matchSubpaths, +})); -// Guard packages that must never fall back to `_internal-shims.d.ts`. -// `@superdoc/pm-adapter` is guarded as a root package even though only -// two exact subpaths are relocated today; a future bare-barrel leak should -// fail the build rather than ship as `any`. -const RELOCATION_GUARD_PACKAGES = [ - '@superdoc/document-api', - '@superdoc/contracts', - '@superdoc/dom-contract', - '@superdoc/layout-bridge', - '@superdoc/layout-engine', - '@superdoc/painter-dom', - '@superdoc/pm-adapter', - '@superdoc/style-engine', - '@superdoc/common', - '@superdoc/common/list-marker-utils', -]; +// Guard packages that must never appear as a `declare module` block in +// `_internal-shims.d.ts`. SD-2942 removed the shim emit; this list is +// kept as defense against stale tarballs and future re-introduction. +const RELOCATION_GUARD_PACKAGES = typeSurface.relocationGuardPackages; function isRelocatedSpecifier(mod) { return RELOCATION_RULES.some((rule) => @@ -356,11 +297,9 @@ const RELOCATION_REWRITERS = RELOCATION_RULES.map((rule) => ({ // Any root specifier added here should also be listed in // RELOCATION_GUARD_PACKAGES so it cannot fall back to an ambient `any` -// shim after we intentionally skip shim generation. -const UNSHIMMED_PRIVATE_SPECIFIERS = new Set([ - '@superdoc/pm-adapter', - '@superdoc/style-engine', -]); +// shim after we intentionally skip shim generation. List source: +// type-surface.config.cjs (`unshimmedPrivateSpecifiers`). +const UNSHIMMED_PRIVATE_SPECIFIERS = new Set(typeSurface.unshimmedPrivateSpecifiers); function shouldSkipWorkspaceShim(mod) { return ( diff --git a/packages/superdoc/scripts/type-surface.config.cjs b/packages/superdoc/scripts/type-surface.config.cjs new file mode 100644 index 0000000000..7fd22bfbe2 --- /dev/null +++ b/packages/superdoc/scripts/type-surface.config.cjs @@ -0,0 +1,222 @@ +/** + * SD-2864: single source of truth for the published superdoc type surface. + * + * The same taxonomy was previously duplicated across four files: + * - packages/superdoc/scripts/ensure-types.cjs + * - packages/superdoc/scripts/audit-declarations.cjs + * - packages/superdoc/vite.config.js + * - packages/superdoc/tsconfig.json + * + * Adding a new public-surface relocation required coordinated edits to all + * four. PR #3144 (pm-adapter) and several SD-2893 stack PRs each shipped a + * regression caused by drift between these lists. This config consolidates + * the canonical data so each consumer derives what it needs from a single + * place. + * + * Shape: + * - `requiredEntryPoints`: dist d.ts paths that must exist after build. + * - `handwrittenDtsBlocklist`: filenames in source that must NOT be + * copied into dist (internal-only ambient declarations). + * - `relocations`: workspace packages whose types appear on the public + * surface. Each entry pairs the rewriter rule with the source paths + * vite-plugin-dts and tsconfig.json need to include for the + * declarations to be emitted into superdoc/dist. + * - `sharedCommonDtsTargets`: filenames in `shared/common/` that the + * postbuild step compiles via tsc. Used for relocations whose source + * lives outside `packages/` and would otherwise shift the + * vite-plugin-dts common-ancestor. + * - `relocationGuardPackages`: packages that must never appear as a + * `declare module` block in `_internal-shims.d.ts` (audit Rule 3). + * SD-2942 removed the shim mechanism; this list is kept as defense + * against stale tarballs and future re-introduction. + * - `unshimmedPrivateSpecifiers`: bare specifiers that, if a future + * mechanism re-introduces shim generation, must not be auto-shimmed. + * They should fail audit Rule 1 instead. + * - `rule1Allowlist`: bare `@superdoc/*` specifiers permitted in + * published d.ts. Currently only the legacy public super-editor + * surface per RFC Decision 1. + * + * Adding a new relocation: append one entry to `relocations` with the + * package specifier, the dist target the rewriter should point at, and + * the source-include patterns vite + tsconfig need. Every consumer picks + * up the new entry without further edits. + */ + +const requiredEntryPoints = [ + 'superdoc/src/index.d.ts', + 'superdoc/src/super-editor.d.ts', + 'super-editor/src/index.d.ts', + 'super-editor/src/types.d.ts', +]; + +const handwrittenDtsBlocklist = [ + // Ambient module declarations for internal `@superdoc/super-editor/converter/internal/...` + // subpaths. Nothing in superdoc's shipped surface imports those subpaths, + // so the declarations would only leak the bare specifiers into published d.ts. + // Keep the file in source for super-editor's own typecheck; just don't ship it. (SD-2859) + 'converter-internal.d.ts', +]; + +/** + * Each relocation describes a private workspace package whose types appear + * on the public surface, plus the source patterns needed to emit those + * declarations into superdoc/dist. + * + * - `pkg`: the bare specifier consumers' d.ts files reference. + * - `distEntry`: the file path (relative to dist root) that the rewriter + * redirects bare specifiers to. Must be emitted by either vite-plugin-dts + * (full-glob include) or the postbuild tsc step (sharedCommonDtsTargets). + * - `matchSubpaths`: when true, the rewriter also rewrites + * `pkg/` → `distEntry` with `/index.js` swapped for `/`. + * When false, only the exact `pkg` specifier is rewritten; subpaths + * fall through to audit Rule 1 (used for narrow relocations like + * pm-adapter where only specific subpaths are emitted). + * - `viteIncludes`: glob/file patterns added to vite-plugin-dts's + * `include` array. Multiple entries are allowed when the relocation + * needs sibling files (e.g. style-engine/ooxml depends on cascade.ts). + * - `tsconfigIncludes`: parallel paths added to tsconfig.json's + * `include` array. The check-tsconfig script verifies parity. + */ +const relocations = [ + { + pkg: '@superdoc/contracts', + distEntry: 'layout-engine/contracts/src/index.d.ts', + matchSubpaths: true, + viteIncludes: ['../layout-engine/contracts/src/**/*'], + tsconfigIncludes: ['../layout-engine/contracts/src'], + }, + { + pkg: '@superdoc/dom-contract', + distEntry: 'layout-engine/dom-contract/src/index.d.ts', + matchSubpaths: true, + viteIncludes: ['../layout-engine/dom-contract/src/**/*'], + tsconfigIncludes: ['../layout-engine/dom-contract/src'], + }, + { + pkg: '@superdoc/layout-bridge', + distEntry: 'layout-engine/layout-bridge/src/index.d.ts', + matchSubpaths: true, + viteIncludes: ['../layout-engine/layout-bridge/src/**/*'], + tsconfigIncludes: ['../layout-engine/layout-bridge/src'], + }, + { + pkg: '@superdoc/layout-engine', + distEntry: 'layout-engine/layout-engine/src/index.d.ts', + matchSubpaths: true, + viteIncludes: ['../layout-engine/layout-engine/src/**/*'], + tsconfigIncludes: ['../layout-engine/layout-engine/src'], + }, + { + pkg: '@superdoc/painter-dom', + distEntry: 'layout-engine/painters/dom/src/index.d.ts', + matchSubpaths: true, + viteIncludes: ['../layout-engine/painters/dom/src/**/*'], + tsconfigIncludes: ['../layout-engine/painters/dom/src'], + }, + // pm-adapter: subpath-only. The full barrel pulls in @superdoc/style-engine + // and other internal packages that would re-expand the shim list. + { + pkg: '@superdoc/pm-adapter/converter-context.js', + distEntry: 'layout-engine/pm-adapter/src/converter-context.d.ts', + matchSubpaths: false, + viteIncludes: ['../layout-engine/pm-adapter/src/converter-context.ts'], + tsconfigIncludes: ['../layout-engine/pm-adapter/src/converter-context.ts'], + }, + { + pkg: '@superdoc/pm-adapter/sections/types.js', + distEntry: 'layout-engine/pm-adapter/src/sections/types.d.ts', + matchSubpaths: false, + viteIncludes: ['../layout-engine/pm-adapter/src/sections/types.ts'], + tsconfigIncludes: ['../layout-engine/pm-adapter/src/sections/types.ts'], + }, + // style-engine/ooxml: subpath-only. Includes the ooxml subtree plus the + // sibling cascade.ts dependency it imports. + { + pkg: '@superdoc/style-engine/ooxml', + distEntry: 'layout-engine/style-engine/src/ooxml/index.d.ts', + matchSubpaths: false, + viteIncludes: [ + '../layout-engine/style-engine/src/ooxml/**/*', + '../layout-engine/style-engine/src/cascade.ts', + ], + tsconfigIncludes: [ + '../layout-engine/style-engine/src/ooxml', + '../layout-engine/style-engine/src/cascade.ts', + ], + }, + // common/list-marker-utils and common (bare): emitted via tsc-postbuild + // (see sharedCommonDtsTargets) because the source lives in shared/, which + // would shift the vite-plugin-dts common-ancestor if added to vite include. + { + pkg: '@superdoc/common/list-marker-utils', + distEntry: 'shared/common/list-marker-utils.d.ts', + matchSubpaths: false, + viteIncludes: [], + tsconfigIncludes: [], + }, + { + pkg: '@superdoc/common', + distEntry: 'shared/common/comments-types.d.ts', + matchSubpaths: false, + viteIncludes: [], + tsconfigIncludes: [], + }, +]; + +/** + * Filenames in `shared/common/` that the postbuild tsc step compiles into + * `dist/shared/common/`. Each filename pairs with a `relocations` entry + * whose `distEntry` lives at `shared/common/.d.ts`. + */ +const sharedCommonDtsTargets = [ + 'list-marker-utils.ts', + 'layout-constants.ts', // dependency of list-marker-utils + 'comments-types.ts', +]; + +/** + * Packages that must NEVER appear as a `declare module` entry in + * `_internal-shims.d.ts`. After SD-2942 the shim file is no longer emitted, + * so this list is a defense against stale tarballs and future + * re-introduction. Mirrored automatically by ensure-types and audit. + */ +const relocationGuardPackages = [ + '@superdoc/document-api', + '@superdoc/contracts', + '@superdoc/dom-contract', + '@superdoc/layout-bridge', + '@superdoc/layout-engine', + '@superdoc/painter-dom', + '@superdoc/pm-adapter', + '@superdoc/style-engine', + '@superdoc/common', + '@superdoc/common/list-marker-utils', +]; + +/** + * Bare specifiers that any future shim-generation mechanism must NOT + * shim. They should fail audit Rule 1 instead. Used today only as + * forward-compat documentation; the SD-2942 removal made shim + * generation a no-op. + */ +const unshimmedPrivateSpecifiers = ['@superdoc/pm-adapter', '@superdoc/style-engine']; + +/** + * Bare `@superdoc/*` specifiers permitted in published d.ts beyond the + * relocation rules. Currently only the legacy public super-editor surface + * per RFC Decision 1; consumers resolve it through the `superdoc/super-editor` + * subpath export at runtime. + */ +const rule1Allowlist = { + '@superdoc/super-editor': 'legacy public surface (RFC Decision 1)', +}; + +module.exports = { + requiredEntryPoints, + handwrittenDtsBlocklist, + relocations, + sharedCommonDtsTargets, + relocationGuardPackages, + unshimmedPrivateSpecifiers, + rule1Allowlist, +}; diff --git a/packages/superdoc/vite.config.js b/packages/superdoc/vite.config.js index 177fd6b22a..7f197a12ed 100644 --- a/packages/superdoc/vite.config.js +++ b/packages/superdoc/vite.config.js @@ -13,6 +13,12 @@ import vue from '@vitejs/plugin-vue' import { version } from './package.json'; import sourceResolve from '../../vite.sourceResolve'; +// SD-2864: derive the dts include list from the canonical type-surface +// config so vite, ensure-types, audit, and the tsconfig parity check +// share one source of truth for relocations. +const cjsRequire = createRequire(import.meta.url); +const typeSurface = cjsRequire('./scripts/type-surface.config.cjs'); + // WORKAROUND: rolldown doesn't support trailing-slash imports (e.g. 'punycode/') // which Node.js treats as "resolve the package entry point". node-stdlib-browser's // url polyfill uses `import from 'punycode/'` and rolldown tries to open the @@ -113,39 +119,16 @@ export default defineConfig(({ mode, command }) => { const plugins = [ vue(), !skipDts && dts({ - // SD-2815: include `../document-api/src/**/*` so the doc-api - // types re-exported from `superdoc/ui` (CommentInfo, Receipt, - // SelectionInfo, TextTarget, etc.) emit real declarations into - // the published dist instead of falling through to the - // `_internal-shims.d.ts` `any` fallback that ensure-types.cjs - // generates for every unshipped `@superdoc/*` package. Without - // this, packed consumers see `any` for those public types and - // the new re-export surface adds no actual checking. + // Foundational sources (superdoc, super-editor, document-api) are + // always included; relocation patterns come from the canonical + // type-surface config (SD-2864). Each `relocations` entry pairs the + // ensure-types rewriter rule with the vite include patterns so the + // two cannot drift. include: [ 'src/**/*', '../super-editor/src/**/*', '../document-api/src/**/*', - // SD-2842: relocate workspace packages whose types appear on the - // public surface so they emit into superdoc's dist and the - // rewrite step in ensure-types can redirect bare specifiers to - // local relative paths. Same pattern as @superdoc/document-api. - '../layout-engine/contracts/src/**/*', - '../layout-engine/dom-contract/src/**/*', - '../layout-engine/layout-bridge/src/**/*', - '../layout-engine/layout-engine/src/**/*', - '../layout-engine/painters/dom/src/**/*', - // SD-2893: pm-adapter is included file-by-file (not via `src/**/*`) - // because the full barrel pulls in @superdoc/style-engine and other - // internal packages that would re-expand the shim list. Only the - // type subpaths reachable from the public surface are relocated. - '../layout-engine/pm-adapter/src/converter-context.ts', - '../layout-engine/pm-adapter/src/sections/types.ts', - // SD-2893: only the /ooxml subpath of style-engine is publicly - // reachable today. Include the ooxml subtree plus the cascade.ts - // sibling it depends on. The full src/**/* glob pulls the broader - // project graph through contracts project references. - '../layout-engine/style-engine/src/ooxml/**/*', - '../layout-engine/style-engine/src/cascade.ts', + ...typeSurface.relocations.flatMap((r) => r.viteIncludes), ], outDir: 'dist', // vite-plugin-dts still gathers diagnostics for this mixed JS/Vue source From 7ef75016fff3841a0602c521a44ed30573db4ceb Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 5 May 2026 12:35:41 -0300 Subject: [PATCH 4/6] refactor(types): derive blocklist and tsc-postbuild targets from config (SD-2864) The previous commit's claim that these two lists derive from type-surface.config.cjs was wrong: the inline literals in ensure-types.cjs were never replaced. The build kept passing because the values matched, but a contributor editing the config without also editing the script would silently land a half-applied change. Replace the inline `new Set([...])` for HANDWRITTEN_DTS_BLOCKLIST and the inline string array for SHARED_COMMON_DTS_TARGETS with references to the config's handwrittenDtsBlocklist and sharedCommonDtsTargets so the original commit's claims hold. Also annotate the two @superdoc/common relocation entries' empty viteIncludes/tsconfigIncludes arrays inline so a reader does not have to walk back to the module-level JSDoc to learn they are emitted via the tsc-postbuild path. --- packages/superdoc/scripts/ensure-types.cjs | 25 ++++++------------- .../superdoc/scripts/type-surface.config.cjs | 6 +++-- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs index 507ba0d949..2878d8c8af 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -18,19 +18,9 @@ const repoRoot = path.resolve(__dirname, '..', '..', '..'); // `core-command-map.d.ts` is referenced via a relative import from another // emitted `.d.ts`, the consumer hits an unresolved-module error. Copy // every hand-written `.d.ts` from the source trees we publish into the -// matching dist location so those imports resolve. -// Hand-written `.d.ts` files we know are internal-only and must NOT ship -// in `superdoc`'s published dist. The copy step is opt-in via filename -// blocklist (rather than e.g. a per-file directive) so future hand-written -// declarations land in dist by default and the cost of skipping one is one -// line here. Each entry should have a comment explaining why. -const HANDWRITTEN_DTS_BLOCKLIST = new Set([ - // Ambient module declarations for internal `@superdoc/super-editor/converter/internal/...` - // subpaths. Nothing in `superdoc`'s shipped surface actually imports those subpaths, - // so the declarations would only leak the bare specifiers into published d.ts. - // Keep the file in source for super-editor's own typecheck; just don't ship it. (SD-2859) - 'converter-internal.d.ts', -]); +// matching dist location so those imports resolve. Source list: +// type-surface.config.cjs `handwrittenDtsBlocklist`. +const HANDWRITTEN_DTS_BLOCKLIST = new Set(typeSurface.handwrittenDtsBlocklist); function copyHandwrittenDtsFiles(srcDir, destDir) { let copied = 0; @@ -71,11 +61,10 @@ if (handwrittenCopiedSuperEditor > 0) { // public surface. Adding shared/ to vite-plugin-dts's `include` would shift the // common-ancestor of all source files to the repo root and reorganise the // entire dist tree, so we run tsc directly for just the files we relocate. -// Today: list-marker-utils plus its sibling layout-constants, and -// comments-types (the four Comment* types referenced via bare @superdoc/common -// imports in three internal-only dist d.ts files). Add new entries here in -// lockstep with `RELOCATION_RULES` below. -const SHARED_COMMON_DTS_TARGETS = ['list-marker-utils.ts', 'layout-constants.ts', 'comments-types.ts']; +// Source list: type-surface.config.cjs `sharedCommonDtsTargets`. Each entry +// pairs with a `relocations` rule whose distEntry points at +// `shared/common/.d.ts`. +const SHARED_COMMON_DTS_TARGETS = typeSurface.sharedCommonDtsTargets; { const { spawnSync: _spawnSync } = require('node:child_process'); const tscBin = path.join(repoRoot, 'node_modules', '.bin', 'tsc'); diff --git a/packages/superdoc/scripts/type-surface.config.cjs b/packages/superdoc/scripts/type-surface.config.cjs index 7fd22bfbe2..66d94fed20 100644 --- a/packages/superdoc/scripts/type-surface.config.cjs +++ b/packages/superdoc/scripts/type-surface.config.cjs @@ -147,18 +147,20 @@ const relocations = [ // common/list-marker-utils and common (bare): emitted via tsc-postbuild // (see sharedCommonDtsTargets) because the source lives in shared/, which // would shift the vite-plugin-dts common-ancestor if added to vite include. + // Empty viteIncludes/tsconfigIncludes are deliberate: ensure-types.cjs's + // tsc-postbuild step handles emit; no vite/tsconfig participation needed. { pkg: '@superdoc/common/list-marker-utils', distEntry: 'shared/common/list-marker-utils.d.ts', matchSubpaths: false, - viteIncludes: [], + viteIncludes: [], // emitted via sharedCommonDtsTargets tsc-postbuild tsconfigIncludes: [], }, { pkg: '@superdoc/common', distEntry: 'shared/common/comments-types.d.ts', matchSubpaths: false, - viteIncludes: [], + viteIncludes: [], // emitted via sharedCommonDtsTargets tsc-postbuild tsconfigIncludes: [], }, ]; From beda7b9f7086d9635de4d5f6b75b1005cfad0f1e Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 5 May 2026 12:41:00 -0300 Subject: [PATCH 5/6] refactor(types): derive requiredEntryPoints from config (SD-2864) The previous fix commit caught HANDWRITTEN_DTS_BLOCKLIST and SHARED_COMMON_DTS_TARGETS but missed requiredEntryPoints, which remained inline. With this commit all three lists in ensure-types.cjs that the SD-2864 PR description claims to derive from the config actually do. Verified: build clean, the three list lengths read from type-surface.config.cjs match what was previously hardcoded (requiredEntryPoints: 4, sharedCommonDtsTargets: 3, handwrittenDtsBlocklist: 1). --- packages/superdoc/scripts/ensure-types.cjs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/superdoc/scripts/ensure-types.cjs b/packages/superdoc/scripts/ensure-types.cjs index 2878d8c8af..8b85d0f2b3 100644 --- a/packages/superdoc/scripts/ensure-types.cjs +++ b/packages/superdoc/scripts/ensure-types.cjs @@ -93,12 +93,7 @@ const SHARED_COMMON_DTS_TARGETS = typeSurface.sharedCommonDtsTargets; console.log(`[ensure-types] ✓ Emitted ${SHARED_COMMON_DTS_TARGETS.length} shared/common declarations`); } -const requiredEntryPoints = [ - 'superdoc/src/index.d.ts', - 'superdoc/src/super-editor.d.ts', - 'super-editor/src/index.d.ts', - 'super-editor/src/types.d.ts', -]; +const requiredEntryPoints = typeSurface.requiredEntryPoints; for (const entry of requiredEntryPoints) { const fullPath = path.join(distRoot, entry); From 4ac5d32077614b27c5cd0094c10912d6e7230728 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 5 May 2026 13:30:01 -0300 Subject: [PATCH 6/6] fix(types): tsconfig parity check now bidirectional (SD-2864) The original check only fired on missing-entry drift (config requires X, tsconfig is missing X). It tolerated stale-entry drift (tsconfig has Y, config no longer requires Y), undermining the single-source-of- truth claim: a relocation removed from type-surface.config.cjs could leave a stale tsconfig include compiling against source the type surface no longer claims to manage. Add baseTsconfigIncludes to type-surface.config.cjs for the foundational source roots that aren't relocations (`src`, `../super-editor/src`, `../document-api/src`). The check now asserts tsconfig.include equals exactly baseTsconfigIncludes plus relocations[*].tsconfigIncludes - no more, no less. Catches three drift modes now: 1. New relocation in config but not mirrored in tsconfig. 2. Relocation removed from config but tsconfig entry left stale. 3. Foundational base entry dropped from tsconfig by mistake. Verified end-to-end: - Clean build passes (12 expected paths matched exactly) - Synthetic missing-entry drift exits 1 with named owner - Synthetic stale-entry drift exits 1 with remediation hint - Restored exits 0 --- .../scripts/check-tsconfig-type-surface.cjs | 63 +++++++++++++------ .../superdoc/scripts/type-surface.config.cjs | 11 ++++ 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/packages/superdoc/scripts/check-tsconfig-type-surface.cjs b/packages/superdoc/scripts/check-tsconfig-type-surface.cjs index 5833807a96..42441c1ef5 100644 --- a/packages/superdoc/scripts/check-tsconfig-type-surface.cjs +++ b/packages/superdoc/scripts/check-tsconfig-type-surface.cjs @@ -1,21 +1,28 @@ #!/usr/bin/env node /** - * SD-2864: enforce parity between tsconfig.json's `include` array and the - * relocation entries in `type-surface.config.cjs`. tsconfig.json is the - * one consumer of the type-surface taxonomy that has no scripting layer - * (it's plain JSON), so we don't generate it; instead this check fails - * the build if the on-disk file drifts from the config. + * SD-2864: enforce parity between tsconfig.json's `include` array and + * `type-surface.config.cjs`. tsconfig.json is the one consumer of the + * type-surface taxonomy that has no scripting layer (it's plain JSON), + * so we don't generate it; this check fails the build if the on-disk + * file drifts from the config in either direction. * - * Allowed shape: tsconfig.json's `include` MUST contain every entry in - * each relocation's `tsconfigIncludes`. Additional entries (the - * foundational `src`, `../super-editor/src`, `../document-api/src`, - * etc.) are tolerated - we only enforce the relocation taxonomy. + * Expected shape: tsconfig.json's `include` MUST equal exactly the + * union of `baseTsconfigIncludes` (foundational sources) and + * `relocations[*].tsconfigIncludes` (per-relocation paths). No more, + * no less. * * Drift modes this catches: - * - A new relocation added to the config but not mirrored in tsconfig.json - * (the typecheck for that source tree would silently miss it). - * - A relocation removed from the config but its tsconfig.json entry left - * stale (less severe but still drift). + * - A new relocation added to the config but not mirrored in + * tsconfig.json (typecheck for that source tree silently misses it). + * - A relocation removed from the config but its tsconfig.json entry + * left stale (entry would compile against source the type-surface + * no longer claims to manage; undermines single-source-of-truth). + * - Foundational base entries dropped from tsconfig.json by mistake. + * + * If foundational entries beyond the current three are needed (e.g. a + * future public package source root), add them to `baseTsconfigIncludes` + * in type-surface.config.cjs rather than carrying them only in + * tsconfig.json. */ const fs = require('node:fs'); @@ -27,17 +34,35 @@ const typeSurface = require('./type-surface.config.cjs'); const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf8')); const tsconfigIncludes = new Set(tsconfig.include || []); -const expected = typeSurface.relocations.flatMap((r) => r.tsconfigIncludes); -const missing = expected.filter((entry) => !tsconfigIncludes.has(entry)); +const relocationIncludes = typeSurface.relocations.flatMap((r) => r.tsconfigIncludes); +const expected = new Set([...typeSurface.baseTsconfigIncludes, ...relocationIncludes]); + +const missing = [...expected].filter((entry) => !tsconfigIncludes.has(entry)); +const stale = [...tsconfigIncludes].filter((entry) => !expected.has(entry)); + +let failed = false; if (missing.length > 0) { - console.error('[check-tsconfig-type-surface] tsconfig.json `include` is missing relocation entries:'); + failed = true; + console.error('[check-tsconfig-type-surface] tsconfig.json `include` is missing entries required by type-surface.config.cjs:'); for (const entry of missing) { const owner = typeSurface.relocations.find((r) => r.tsconfigIncludes.includes(entry)); - console.error(` - ${entry} (required by ${owner.pkg})`); + const reason = owner ? `required by ${owner.pkg}` : 'foundational base include'; + console.error(` - ${entry} (${reason})`); + } +} + +if (stale.length > 0) { + failed = true; + console.error('[check-tsconfig-type-surface] tsconfig.json `include` has entries not declared in type-surface.config.cjs:'); + for (const entry of stale) { + console.error(` - ${entry} (stale - remove from tsconfig.json or add to baseTsconfigIncludes / a relocation)`); } - console.error('Add the entries above to packages/superdoc/tsconfig.json or update type-surface.config.cjs.'); +} + +if (failed) { + console.error('Update packages/superdoc/tsconfig.json or packages/superdoc/scripts/type-surface.config.cjs so the two stay in sync.'); process.exit(1); } -console.log(`[check-tsconfig-type-surface] ✓ tsconfig.json mirrors ${expected.length} relocation include paths`); +console.log(`[check-tsconfig-type-surface] ✓ tsconfig.json mirrors ${expected.size} type-surface include paths exactly`); diff --git a/packages/superdoc/scripts/type-surface.config.cjs b/packages/superdoc/scripts/type-surface.config.cjs index 66d94fed20..b671cb336e 100644 --- a/packages/superdoc/scripts/type-surface.config.cjs +++ b/packages/superdoc/scripts/type-surface.config.cjs @@ -49,6 +49,16 @@ const requiredEntryPoints = [ 'super-editor/src/types.d.ts', ]; +/** + * Foundational source roots tsconfig.json must include but `relocations` + * does not own. These are the public-package sources themselves + * (`superdoc/src`, `super-editor/src`, `document-api/src`), distinct from + * the workspace-internal packages relocated via `relocations`. The + * tsconfig parity check expects exactly this base set plus the union of + * `relocations[*].tsconfigIncludes`. + */ +const baseTsconfigIncludes = ['src', '../super-editor/src', '../document-api/src']; + const handwrittenDtsBlocklist = [ // Ambient module declarations for internal `@superdoc/super-editor/converter/internal/...` // subpaths. Nothing in superdoc's shipped surface imports those subpaths, @@ -216,6 +226,7 @@ const rule1Allowlist = { module.exports = { requiredEntryPoints, handwrittenDtsBlocklist, + baseTsconfigIncludes, relocations, sharedCommonDtsTargets, relocationGuardPackages,