3333 *
3434 * Adding a new facade file:
3535 * - Create `packages/superdoc/src/public/<name>.ts` with named exports.
36- * - Wire it into `vite.config.js` (`rollupOptions.input`) and the CJS
37- * shim list in `scripts/ensure-types.cjs`.
36+ * - Wire it into `vite.config.js` (`rollupOptions.input`).
37+ * - If the new entry is intended to ship with both ESM and CJS type
38+ * declarations (i.e. `package.json#exports` will use a `types.import` /
39+ * `types.require` pair), also add it to `cjsDeclarationShims` in
40+ * `scripts/ensure-types.cjs` and set `cjs` on the `FACADE_ENTRIES`
41+ * entry below. If the entry will use a single `types` string instead
42+ * (matching the SD-3180 legacy leaf entries), leave `cjs: null` and
43+ * the parity check is skipped. Phase 4 of SD-3175 owns the contract
44+ * flip and decides per-entry which shape ships.
3845 * - Append a `FACADE_ENTRIES` entry below with the expected symbol set.
3946 * - If the new entry re-exports `EditorCommands`, set
4047 * `runsCommandSignatureProbe: true`.
@@ -97,6 +104,44 @@ const FACADE_ENTRIES = [
97104 runsCommandSignatureProbe : false ,
98105 ticket : 'SD-3179' ,
99106 } ,
107+ // SD-3180: legacy leaf entries. These match the existing single-types
108+ // pattern of the live `superdoc/converter` / `superdoc/docx-zipper` /
109+ // `superdoc/file-zipper` subpaths, which do not have `.d.cts` shims
110+ // today. `cjs: null` skips the ESM/CJS parity check. Phase 4 decides
111+ // whether to add CJS shims when the contract flips.
112+ {
113+ name : 'legacy/converter' ,
114+ esm : path . join ( PUBLIC_DIST , 'legacy' , 'converter.d.ts' ) ,
115+ cjs : null ,
116+ // AIDEV-NOTE: `hasBodyNumberingReferences` is in the runtime contract
117+ // of today's `superdoc/converter` (see
118+ // `packages/superdoc/dist/super-editor/converter.es.js`) but missing
119+ // from the existing types entry. The facade types both so Phase 4
120+ // can flip without regressing JS consumers.
121+ expectedNames : [ 'SuperConverter' , 'hasBodyNumberingReferences' ] ,
122+ runsCommandSignatureProbe : false ,
123+ ticket : 'SD-3180' ,
124+ } ,
125+ {
126+ name : 'legacy/docx-zipper' ,
127+ esm : path . join ( PUBLIC_DIST , 'legacy' , 'docx-zipper.d.ts' ) ,
128+ cjs : null ,
129+ // AIDEV-NOTE: `default`, not `DocxZipper`. The current public contract
130+ // is `import DocxZipper from 'superdoc/docx-zipper'`. The resolved
131+ // exported name is therefore `default`. Changing to a named export
132+ // would break consumers.
133+ expectedNames : [ 'default' ] ,
134+ runsCommandSignatureProbe : false ,
135+ ticket : 'SD-3180' ,
136+ } ,
137+ {
138+ name : 'legacy/file-zipper' ,
139+ esm : path . join ( PUBLIC_DIST , 'legacy' , 'file-zipper.d.ts' ) ,
140+ cjs : null ,
141+ expectedNames : [ 'createZip' ] ,
142+ runsCommandSignatureProbe : false ,
143+ ticket : 'SD-3180' ,
144+ } ,
100145] ;
101146
102147function loadFile ( file ) {
@@ -108,27 +153,55 @@ function loadFile(file) {
108153 return fs . readFileSync ( file , 'utf8' ) ;
109154}
110155
111- function listExportedNames ( file ) {
156+ function formatDiagnostic ( diagnostic ) {
157+ const message = ts . flattenDiagnosticMessageText ( diagnostic . messageText , '\n' ) ;
158+ if ( ! diagnostic . file || diagnostic . start == null ) return message ;
159+ const { line, character } = diagnostic . file . getLineAndCharacterOfPosition ( diagnostic . start ) ;
160+ const relative = path . relative ( repoRoot , diagnostic . file . fileName ) ;
161+ return `${ relative } :${ line + 1 } :${ character + 1 } ${ message } ` ;
162+ }
163+
164+ function listExportedNames ( entry , file ) {
112165 const program = ts . createProgram ( {
113166 rootNames : [ file ] ,
114167 options : {
115168 moduleResolution : ts . ModuleResolutionKind . Bundler ,
116169 module : ts . ModuleKind . ESNext ,
117170 target : ts . ScriptTarget . ESNext ,
118171 noEmit : true ,
119- skipLibCheck : true ,
172+ skipLibCheck : false ,
120173 } ,
121174 } ) ;
175+ const diagnostics = [
176+ ...program . getSyntacticDiagnostics ( ) ,
177+ ...program . getSemanticDiagnostics ( ) ,
178+ ...program . getDeclarationDiagnostics ( ) ,
179+ ] ;
180+ if ( diagnostics . length > 0 ) {
181+ console . error ( `[verify-public-facade-emit] ${ entry . name } : facade declaration has TypeScript diagnostics.` ) ;
182+ for ( const diagnostic of diagnostics . slice ( 0 , 10 ) ) {
183+ console . error ( ' - ' + formatDiagnostic ( diagnostic ) ) ;
184+ }
185+ if ( diagnostics . length > 10 ) {
186+ console . error ( ` ... ${ diagnostics . length - 10 } more diagnostics` ) ;
187+ }
188+ return { ok : false , names : [ ] } ;
189+ }
122190 const checker = program . getTypeChecker ( ) ;
123191 const src = program . getSourceFile ( file ) ;
124192 const symbol = checker . getSymbolAtLocation ( src ) ?? ( src && src . symbol ) ;
125- if ( ! symbol ) return [ ] ;
126- return [ ...new Set ( checker . getExportsOfModule ( symbol ) . map ( ( s ) => s . getName ( ) ) ) ] . sort ( ) ;
193+ if ( ! symbol ) return { ok : true , names : [ ] } ;
194+ return {
195+ ok : true ,
196+ names : [ ...new Set ( checker . getExportsOfModule ( symbol ) . map ( ( s ) => s . getName ( ) ) ) ] . sort ( ) ,
197+ } ;
127198}
128199
129200function checkSymbolSet ( entry ) {
130201 const expected = [ ...entry . expectedNames ] . sort ( ) ;
131- const actual = listExportedNames ( entry . esm ) ;
202+ const result = listExportedNames ( entry , entry . esm ) ;
203+ if ( ! result . ok ) return { ok : false , actual : result . names } ;
204+ const actual = result . names ;
132205 if ( JSON . stringify ( actual ) === JSON . stringify ( expected ) ) {
133206 return { ok : true , actual } ;
134207 }
@@ -141,7 +214,9 @@ function checkSymbolSet(entry) {
141214}
142215
143216function checkEsmCjsParity ( entry , esmNames ) {
144- const cjsNames = listExportedNames ( entry . cjs ) ;
217+ const result = listExportedNames ( entry , entry . cjs ) ;
218+ if ( ! result . ok ) return false ;
219+ const cjsNames = result . names ;
145220 if ( JSON . stringify ( esmNames ) === JSON . stringify ( cjsNames ) ) return true ;
146221 const importOnly = esmNames . filter ( ( n ) => ! cjsNames . includes ( n ) ) ;
147222 const requireOnly = cjsNames . filter ( ( n ) => ! esmNames . includes ( n ) ) ;
@@ -218,7 +293,9 @@ const LEAK_PATTERNS = [
218293
219294function checkLeaks ( entry ) {
220295 let ok = true ;
221- for ( const file of [ entry . esm , entry . cjs ] ) {
296+ const files = [ entry . esm ] ;
297+ if ( entry . cjs ) files . push ( entry . cjs ) ;
298+ for ( const file of files ) {
222299 const code = stripComments ( loadFile ( file ) ) ;
223300 for ( const pattern of LEAK_PATTERNS ) {
224301 const matches = code . match ( pattern . re ) ;
@@ -239,7 +316,10 @@ for (const entry of FACADE_ENTRIES) {
239316 const symbolResult = checkSymbolSet ( entry ) ;
240317 if ( ! symbolResult . ok ) failed = true ;
241318
242- if ( ! checkEsmCjsParity ( entry , symbolResult . actual ) ) failed = true ;
319+ // Entries with `cjs: null` (e.g. SD-3180 legacy leaf entries that match
320+ // the existing single-types pattern) skip the parity check until Phase 4
321+ // decides whether to add proper CJS shims.
322+ if ( entry . cjs && ! checkEsmCjsParity ( entry , symbolResult . actual ) ) failed = true ;
243323
244324 if ( entry . runsCommandSignatureProbe && ! checkCommandSignatureProbe ( entry ) ) {
245325 failed = true ;
0 commit comments