refactor(typecheck): derive facade expected names from source files#3476
Merged
caio-pizzol merged 1 commit intoMay 24, 2026
Merged
Conversation
verify-public-facade-emit.cjs previously hand-maintained 426 expected symbol names across 10 facades (root: 204, types: 116, ui: 71, ui-react: 13, legacy/headless-toolbar: 16, plus 5 small entries). The same names already lived in the facade source files at packages/superdoc/src/public/**, so every facade change required updating two places. The script now parses each facade source file with the TypeScript AST and derives the expected set directly. `source: path.join(PUBLIC_SRC, '<name>.ts')` replaces `expectedNames: [...]` on every entry. Wildcard re-exports (`export *` and `export * as X`) are rejected in facade sources so the contract stays explicit and reviewable as source diff. Importantly, expected names are NOT derived from the emitted .d.ts: the gate's purpose is to catch drift between source-declared and emitted exports. Comparing emit to itself would be self-confirming. No-growth assertions stay where they belong: snapshot.mjs (root / legacy / super-editor inventories) and the root-classification closure gate. Other postbuild checks in this script are unchanged: ESM/CJS parity, legacy command-signature probe, typeOnly value-decl detection, leak detection. Net: -320 lines across 12 files (verifier 857 β 533). Manually simulated both failure modes before commit: - dropping hasBodyNumberingReferences from converter.d.ts emit β verifier fails with 'missing from emit: hasBodyNumberingReferences' pointing at source and emit paths. - adding `export *` to file-zipper.ts source β verifier fails with the file:line:col and the 'export * is not allowed in a public facade source' rule. Verified: pnpm --filter superdoc run build PASS (verifier prints OK across 10 entries with the same counts as before); pnpm check:types PASS; pnpm check:public:superdoc --skip-build PASS (9 stages, 8 ran / 1 skipped, 123s).
Codecov Reportβ All modified and coverable lines are covered by tests. π’ Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
verify-public-facade-emit.cjspreviously hand-maintained ~426 expected symbol names across 10 facades. The same names already lived in the facade source files atpackages/superdoc/src/public/**, so every facade change required updating two places.The script now parses each facade source file with the TypeScript AST and derives the expected set directly.
source: path.join(PUBLIC_SRC, '<name>.ts')replacesexpectedNames: [...]on every entry. Wildcard re-exports (export *andexport * as X) are rejected in facade sources so the contract stays explicit and reviewable as source diff.Why this is not self-confirming
Expected names are NOT derived from the emitted
.d.ts: the gate's purpose is to catch drift between source-declared and emitted exports. Comparing emit to itself would be useless. The source.tsfile is a different artifact - committed, reviewable, parsed independently - so the gate still catches dts-pipeline regressions.What the gate still checks
cjsexistssuperdoc/typesremains type-only (noexport declare constin the CJS shim)@superdoc/*workspace specifiers leak.pnpm/, absolute source paths, or local path leaksWhat it does NOT enforce (by design)
No-growth assertions stay where they belong:
snapshot.mjs(root / legacy / super-editor inventories) and the root-classification closure gate. Adding a new export to a facade source is now a normal source change reviewed by the PR diff, not gated by a parallel allowlist update.Footprint
-320 lines net across 12 files. Verifier alone: 857 β 533 lines.
Verified
pnpm --filter superdoc run buildβ PASS. Verifier prints OK across the same 10 entries with the same export counts as before (root 204, types 116, ui 71, ui-react 13, legacy/headless-toolbar 16, legacy/converter 2, legacy/file-zipper 1, legacy/docx-zipper 1, legacy/headless-toolbar-react 1, legacy/headless-toolbar-vue 1).pnpm check:typesβ PASS (tsc -b tsconfig.references.jsonclean).pnpm check:public:superdoc --skip-buildβ PASS, 8 ran / 1 skipped, 123s.hasBodyNumberingReferencesfromdist/.../legacy/converter.d.tsβ verifier failed withmissing from emit: hasBodyNumberingReferencesand pointed at both source and emit paths.export *in source: appendedexport * from '@superdoc/super-editor/file-zipper';tolegacy/file-zipper.tsβ verifier failed withlegacy/file-zipper.ts:18:1 export * is not allowed in a public facade source; list every name explicitly.Review: confirm
parseFacadeSourceExportshandles every shape used by the facade source files (named re-export, type-only re-export,export { default } from,export default ...,export const,export function/class/interface/type/enum). Parser coverage is exercised by the 10 real facade source files during build; those files cover every export shape used today. The simulatedexport *failure covers the main rejected form.Follow-ups not in this PR: the
sourcefield opens the door to havingvite.config.jsderive itsrollupOptions.inputfrom the same FACADE_ENTRIES list. Not done here to keep blast radius small.