Skip to content

Commit b4963d8

Browse files
authored
refactor(types): remove _internal-shims.d.ts soft-landing mechanism (SD-2942) (#3155)
* 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. * docs(types): fix stale comment about @superdoc/common shim path (SD-2942) 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.
1 parent f527db5 commit b4963d8

2 files changed

Lines changed: 50 additions & 167 deletions

File tree

packages/superdoc/scripts/audit-declarations.cjs

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,27 @@
55
* and reports:
66
*
77
* Rule 1 (FAIL in strict mode): private workspace specifier in an emitted
8-
* declaration that is NOT covered by `_internal-shims.d.ts` and NOT a
9-
* legacy public surface. The shim file is the registry of "known
10-
* unresolved" private modules whose types the RFC tolerates collapsing
11-
* to `any`; legacy public surfaces (currently `@superdoc/super-editor`)
12-
* resolve through the published dist tree. Anything outside that
13-
* allowlist is a leak the RFC forbids: a consumer's strict-mode build
14-
* fails to resolve the import.
8+
* declaration that is NOT in `RULE1_ALLOWLIST` (legacy public surfaces,
9+
* currently only `@superdoc/super-editor`). After SD-2942 there is no
10+
* `_internal-shims.d.ts` fallback, so any unrelocated `@superdoc/*`
11+
* specifier on the public surface fails the build instead of riding
12+
* through silently as `any`. If the file is present (a stale dist from
13+
* before SD-2942), its `declare module` entries still suppress Rule 1
14+
* for backward compatibility.
1515
*
1616
* Rule 2 (FAIL in strict mode): package-manager-internal paths.
1717
* `node_modules/.pnpm/...` paths leak the local install layout into a
1818
* declaration that consumers cannot resolve.
1919
*
2020
* Rule 3 (FAIL in strict mode): a relocated package reappears in
21-
* `_internal-shims.d.ts`. The RFC's relocation pattern (SD-2842) routes
22-
* Document API, contracts, layout-bridge, and painter-dom types through
23-
* `superdoc`'s own dist tree; if any of those packages collapse back into
24-
* an `any` shim, customers see the regression. This rule overlaps with
25-
* the build-time check in `ensure-types.cjs`; keeping both lets the audit
26-
* run as a standalone gate against any tarball, not just during a fresh
27-
* build.
21+
* `_internal-shims.d.ts`. With SD-2942 the file is no longer emitted
22+
* by the build, so this rule is a no-op in steady state — kept as a
23+
* defense if a future change re-introduces the file or runs against
24+
* a stale tarball.
2825
*
29-
* Informational: the set of modules still declared in `_internal-shims.d.ts`.
30-
* The shim file may legitimately exist for legacy or internal-only
31-
* declarations; the RFC's audit-gate rule is "no public type may resolve
32-
* through it", not "the file must not exist". This list is reported so
33-
* drift is visible and the surface can be tightened over time, but its
34-
* contents do not fail the audit.
26+
* Informational: the set of modules still declared in `_internal-shims.d.ts`
27+
* when the file exists. After SD-2942 the file is not emitted, so this
28+
* section is normally absent.
3529
*
3630
* Default mode is strict: findings exit non-zero so a regression cannot
3731
* ship silently. Pass `--informational` (or set

packages/superdoc/scripts/ensure-types.cjs

Lines changed: 36 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,15 @@ if (!hasSuperDocExport) {
123123
process.exit(1);
124124
}
125125

126-
// Fix workspace package imports that aren't resolvable by consumers.
127-
// @superdoc/common is a private workspace package — inline its types in
128-
// the main entry. Other reachable d.ts files that import from
129-
// @superdoc/common fall through to the ambient shim block below; those
130-
// imports surface internal types (Comment, CommentContent, CommentJSON)
131-
// that are not on the public surface, so collapsing them to `any` via
132-
// the shim is correct.
126+
// @superdoc/common is a private workspace package, so consumers can't
127+
// resolve a bare `from '@superdoc/common'` import. The main entry
128+
// (superdoc/src/index.d.ts) imports runtime values from it — DOCX/PDF/
129+
// HTML constants, getFileObject, compareVersions, BlankDOCX (the last
130+
// from a Vite `?url` import that vite-plugin-dts can't type). Strip
131+
// that import statement and inline ambient declarations for those
132+
// values. Type-only imports of @superdoc/common from other dist files
133+
// are handled separately by the RELOCATION_RULES rewriter below, which
134+
// maps bare @superdoc/common to dist/shared/common/comments-types.d.ts.
133135
const hadWorkspaceImport = content.includes('@superdoc/common');
134136
if (hadWorkspaceImport) {
135137
// Replace the @superdoc/common import with inline declarations
@@ -492,148 +494,35 @@ if (fs.readFileSync(superEditorFacadePath, 'utf8') !== expectedSuperEditorFacade
492494
}
493495

494496
// ---------------------------------------------------------------------------
495-
// Generate ambient module declarations for private workspace packages (SD-2227)
496-
//
497-
// Internal .d.ts files reference @superdoc/* workspace packages that consumers
498-
// can't install. Generate a shim so TypeScript can resolve these imports.
499-
// ---------------------------------------------------------------------------
500-
501-
// Collect @superdoc/* workspace module specifiers and their named imports from
502-
// all .d.ts files. These are private packages consumers can't install — we
503-
// generate ambient `declare module` shims for them.
504-
const workspaceImports = new Map(); // module → Set<name>
505-
506-
for (const filePath of dtsFiles) {
507-
const fileContent = fs.readFileSync(filePath, 'utf8');
508-
509-
// Match: import/export { Foo, Bar } from '...' and import/export type { Foo } from '...'
510-
const namedImports = fileContent.matchAll(/(?:import|export)\s+(?:type\s+)?\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g);
511-
for (const m of namedImports) {
512-
const mod = m[2];
513-
514-
// Skip relative imports and already-handled packages
515-
if (shouldSkipWorkspaceShim(mod)) continue;
516-
517-
if (mod.startsWith('@superdoc/')) {
518-
if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set());
519-
const names = m[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
520-
for (const name of names) workspaceImports.get(mod).add(name);
521-
}
522-
}
523-
524-
// Match: import('...').SomeName — dynamic import type references
525-
const dynamicImports = fileContent.matchAll(/import\(['"]([^'"]+)['"]\)\.(\w+)/g);
526-
for (const m of dynamicImports) {
527-
const mod = m[1];
528-
if (shouldSkipWorkspaceShim(mod)) continue;
529-
530-
if (mod.startsWith('@superdoc/')) {
531-
if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set());
532-
workspaceImports.get(mod).add(m[2]);
533-
}
534-
}
535-
536-
// Match bare @superdoc/* module references
537-
const bareRefs = fileContent.matchAll(/['"](@superdoc\/[^'"]+)['"]/g);
538-
for (const m of bareRefs) {
539-
const mod = m[1];
540-
// Skip @superdoc/super-editor (consumer-facing, not internal). All
541-
// other @superdoc/* references (including @superdoc/common root and
542-
// its subpaths) fall through to shim generation. The strip-and-inline
543-
// step above handles `superdoc/src/index.d.ts`'s @superdoc/common
544-
// import explicitly; other files importing from @superdoc/common
545-
// resolve through the shim and collapse internal-only types
546-
// (Comment, CommentContent, CommentJSON) to `any`. None of those
547-
// appear on superdoc's public surface, so the collapse is safe.
548-
if (shouldSkipWorkspaceShim(mod)) continue;
549-
if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set());
550-
}
551-
}
552-
497+
// SD-2942: the auto-generated `_internal-shims.d.ts` mechanism was removed
498+
// after SD-2893 drained every shim entry to zero. Previously this script
499+
// scanned dist d.ts files for `from '@superdoc/...'` patterns and wrote a
500+
// `declare module 'X' { export type Y = any; }` block for each unrelocated
501+
// specifier — the "soft landing" path that quietly collapsed new private
502+
// types to `any`. With SD-2893 complete, every reachable workspace type
503+
// resolves through `RELOCATION_RULES` or stays bare for audit Rule 1 to
504+
// reject. A future PR that introduces a new private `@superdoc/*` import
505+
// is expected to fail the build at `audit-declarations.cjs` rather than
506+
// ride through silently as `any`. The triple-slash reference directive
507+
// previously injected into entry-point d.ts is also dropped; vite-plugin-dts
508+
// emits clean entries and the next build overwrites any stale references.
553509
// ---------------------------------------------------------------------------
554-
// Write _internal-shims.d.ts
555-
//
556-
// Only contains auto-generated shims for @superdoc/* workspace packages.
557-
// External packages (prosemirror-*, vue, eventemitter3, yjs, etc.) are NOT
558-
// shimmed — ambient `declare module` overrides real types globally, breaking
559-
// consumers who depend on those packages (IT-852).
560-
// ---------------------------------------------------------------------------
561-
562-
const shimLines = [
563-
'// Auto-generated ambient declarations for internal workspace packages.',
564-
'// These are private @superdoc/* packages that consumers cannot install.',
565-
'// This file prevents TypeScript errors when skipLibCheck is false.',
566-
'//',
567-
'// External packages (prosemirror-*, vue, eventemitter3, yjs, etc.) are NOT',
568-
'// shimmed here — their real types come from node_modules. Ambient shims for',
569-
'// external packages would override real types globally, breaking consumers',
570-
'// who depend on those packages (e.g. Tiptap users need real prosemirror types).',
571-
'//',
572-
'// NOTE: This is a script file (no exports), so `declare module` creates',
573-
'// global ambient declarations and top-level declarations are global.',
574-
'',
575-
];
576-
577-
// --- Auto-generated @superdoc/* workspace package shims ---
578-
579-
let wsCount = 0;
580-
if (workspaceImports.size > 0) {
581-
shimLines.push('// --- Internal workspace packages (auto-generated) ---');
582-
shimLines.push('');
583-
for (const [mod, names] of [...workspaceImports.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
584-
wsCount++;
585-
const sortedNames = [...names].sort();
586-
const exportLines = [];
587-
for (const n of sortedNames) {
588-
// `default` is a reserved word and cannot appear in `export type
589-
// default = any;`. When a file imports the default export of a
590-
// private module (e.g. `import { default as Foo } from '@superdoc/common/components/Foo.vue'`),
591-
// the named-imports collector picks up `default` as a name; emit
592-
// a proper `export default` declaration instead.
593-
if (n === 'default') {
594-
exportLines.push(' const _default: any;');
595-
exportLines.push(' export default _default;');
596-
} else {
597-
exportLines.push(` export type ${n} = any;`);
598-
}
599-
}
600-
if (exportLines.length > 0) {
601-
shimLines.push(`declare module '${mod}' {\n${exportLines.join('\n')}\n}`);
602-
} else {
603-
shimLines.push(`declare module '${mod}' { const _: any; export default _; }`);
604-
}
605-
}
606-
}
607-
shimLines.push('');
608-
609-
const shimPath = path.join(distRoot, '_internal-shims.d.ts');
610-
fs.writeFileSync(shimPath, shimLines.join('\n'));
611510

612-
// Add reference directive to entry points so TypeScript includes the shims
613-
const shimRef = '/// <reference path="../../_internal-shims.d.ts" />\n';
614-
for (const entry of requiredEntryPoints) {
615-
const entryPath = path.join(distRoot, entry);
616-
const entryContent = fs.readFileSync(entryPath, 'utf8');
617-
if (!entryContent.includes('_internal-shims.d.ts')) {
618-
fs.writeFileSync(entryPath, shimRef + entryContent);
619-
}
620-
}
621-
622-
console.log(`[ensure-types] ✓ Generated ambient shims for ${wsCount} workspace modules`);
623-
624-
// SD-2842 regression net: assert that no relocated package leaked back
625-
// into the shim file. If one shows up, a future change broke the
626-
// rewrite or include for that package and customers would see `any`
627-
// for those types again.
628-
const shimContent = fs.readFileSync(shimPath, 'utf8');
629-
const SHIM_FORBIDDEN = RELOCATION_GUARD_PACKAGES;
630-
for (const pkg of SHIM_FORBIDDEN) {
631-
const re = new RegExp(`declare module '${escapeRegExp(pkg)}(\\/[^']+)?'`);
632-
if (re.test(shimContent)) {
633-
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.`);
634-
process.exit(1);
635-
}
511+
// `shouldSkipWorkspaceShim` is intentionally retained: it is no longer used
512+
// by shim generation, but kept as documentation for the relocation policy
513+
// (relocated specifiers + UNSHIMMED_PRIVATE_SPECIFIERS + super-editor /
514+
// document-api legacy public surface). Future audit rules that need to
515+
// classify workspace specifiers can reuse it.
516+
void shouldSkipWorkspaceShim;
517+
518+
// Clean up artifacts from the old shim mechanism. vite-plugin-dts overwrites
519+
// entry-point d.ts on each build, so the triple-slash references injected by
520+
// the old code are wiped automatically; only the shim file itself persists
521+
// across builds and needs an explicit unlink.
522+
const legacyShimPath = path.join(distRoot, '_internal-shims.d.ts');
523+
if (fs.existsSync(legacyShimPath)) {
524+
fs.unlinkSync(legacyShimPath);
525+
console.log('[ensure-types] ✓ Removed legacy _internal-shims.d.ts');
636526
}
637-
console.log(`[ensure-types] ✓ Verified ${SHIM_FORBIDDEN.length} relocated packages do not appear in shim file`);
638527

639528
console.log('[ensure-types] ✓ Verified type entry points');

0 commit comments

Comments
 (0)