From c6e40fb729f3508046fe45993efe86d08833b2bf Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 5 May 2026 13:04:25 -0300 Subject: [PATCH 1/2] fix(types): ship types for converter/docx-zipper/file-zipper subpaths (SD-2953) The three subpaths were exported at runtime via package.json `exports` but had no `types` field. Strict TypeScript consumers importing any of them got TS7016 ("could not find a declaration file"). The consumer- typecheck matrix didn't catch this because no fixture imported these subpaths. The .d.ts targets already existed in dist (vite-plugin-dts emits them from the super-editor source tree). The fix is to point at them: - ./converter -> dist/super-editor/src/editors/v1/core/super-converter/SuperConverter.d.ts - ./docx-zipper -> dist/super-editor/src/editors/v1/core/DocxZipper.d.ts - ./file-zipper -> dist/super-editor/src/editors/v1/core/super-converter/zipper.d.ts Update both the conditional `exports..types` field and the parallel `typesVersions["*"]` mapping for legacy resolution modes. Add three matrix fixtures (imports-converter.ts, imports-docx-zipper.ts, imports-file-zipper.ts) that import through each public subpath and assert the type resolves to a real declaration (IsAny check). Each runs under bundler + node16 with skipLibCheck:false and strict. Add packages/superdoc/scripts/check-export-coverage.cjs that asserts every package.json `exports` entry has a `types` field, is a CSS asset, or is on RUNTIME_ONLY_ALLOWLIST with a reason. Wires into postbuild so this gap class can't reappear. Negative test: removing a `types` field makes the audit exit 1 with a remediation message. Update docs/architecture/package-boundaries.md to remove the "runtime-only / no type contract" classification for these three subpaths and reference the new fixtures. Verified: build:es clean, consumer matrix 53/0/0 (47 baseline + 6 new SD-2953 scenarios), runtime smoke 3/3 against the packed tarball (SuperConverter, DocxZipper default, createZip), export-coverage audit exits 1 on synthetic drift and 0 when correct. --- docs/architecture/package-boundaries.md | 6 +- packages/superdoc/package.json | 14 +++- .../scripts/check-export-coverage.cjs | 71 +++++++++++++++++++ .../src/imports-converter.ts | 20 ++++++ .../src/imports-docx-zipper.ts | 20 ++++++ .../src/imports-file-zipper.ts | 22 ++++++ tests/consumer-typecheck/typecheck-matrix.mjs | 58 +++++++++++++++ 7 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 packages/superdoc/scripts/check-export-coverage.cjs create mode 100644 tests/consumer-typecheck/src/imports-converter.ts create mode 100644 tests/consumer-typecheck/src/imports-docx-zipper.ts create mode 100644 tests/consumer-typecheck/src/imports-file-zipper.ts diff --git a/docs/architecture/package-boundaries.md b/docs/architecture/package-boundaries.md index d956353741..a99eeb22a1 100644 --- a/docs/architecture/package-boundaries.md +++ b/docs/architecture/package-boundaries.md @@ -112,9 +112,9 @@ The `superdoc` package currently exposes the following entries via `package.json | `./headless-toolbar` | Yes | Public subpath | `imports-headless-toolbar.ts` | Stays | | `./headless-toolbar/react` | Yes | Public subpath | `imports-headless-toolbar-react.ts` | Stays | | `./headless-toolbar/vue` | Yes | Public subpath | `imports-headless-toolbar-vue.ts` | Stays | -| `./converter` | No (runtime-only) | Legacy public compatibility surface | n/a (no type contract) | DOCX conversion is also reachable through `Editor.open` / `Editor.loadXmlData` / `SuperConverter` exported from `superdoc`. Kept exported, not advertised, migration target is `superdoc`. | -| `./docx-zipper` | No (runtime-only) | Legacy public compatibility surface | n/a (no type contract) | `DocxZipper` is exported from `superdoc`. Kept exported, not advertised, migration target is `superdoc`. | -| `./file-zipper` | No (runtime-only) | Legacy public compatibility surface | n/a (no type contract) | `createZip` is exported from `superdoc`. Kept exported, not advertised, migration target is `superdoc`. | +| `./converter` | Yes (SD-2953) | Legacy public compatibility surface | `imports-converter.ts` | DOCX conversion is also reachable through `Editor.open` / `Editor.loadXmlData` / `SuperConverter` exported from `superdoc`. Kept exported, not advertised, migration target is `superdoc`. Types added in SD-2953 to satisfy strict-mode consumers. | +| `./docx-zipper` | Yes (SD-2953) | Legacy public compatibility surface | `imports-docx-zipper.ts` | `DocxZipper` is exported from `superdoc`. Kept exported, not advertised, migration target is `superdoc`. Types added in SD-2953. | +| `./file-zipper` | Yes (SD-2953) | Legacy public compatibility surface | `imports-file-zipper.ts` | `createZip` is exported from `superdoc`. Kept exported, not advertised, migration target is `superdoc`. Types added in SD-2953. | | `./style.css` | N/A | Public asset | n/a (asset) | Stays | When a new subpath is added to `package.json` `exports`, the change must update both this inventory and the consumer matrix in the same PR. SD-2861's matrix scenarios are the gate that fails CI when a typed subpath ships without coverage. diff --git a/packages/superdoc/package.json b/packages/superdoc/package.json index ff3e0c2f2c..772c54cbec 100644 --- a/packages/superdoc/package.json +++ b/packages/superdoc/package.json @@ -31,9 +31,11 @@ "require": "./dist/types.cjs" }, "./converter": { + "types": "./dist/super-editor/src/editors/v1/core/super-converter/SuperConverter.d.ts", "import": "./dist/super-editor/converter.es.js" }, "./docx-zipper": { + "types": "./dist/super-editor/src/editors/v1/core/DocxZipper.d.ts", "import": "./dist/super-editor/docx-zipper.es.js" }, "./super-editor": { @@ -68,6 +70,7 @@ "import": "./dist/headless-toolbar-vue.es.js" }, "./file-zipper": { + "types": "./dist/super-editor/src/editors/v1/core/super-converter/zipper.d.ts", "import": "./dist/super-editor/file-zipper.es.js" }, "./style.css": "./dist/style.css" @@ -95,6 +98,15 @@ ], "types": [ "./dist/super-editor/src/types.d.ts" + ], + "converter": [ + "./dist/super-editor/src/editors/v1/core/super-converter/SuperConverter.d.ts" + ], + "docx-zipper": [ + "./dist/super-editor/src/editors/v1/core/DocxZipper.d.ts" + ], + "file-zipper": [ + "./dist/super-editor/src/editors/v1/core/super-converter/zipper.d.ts" ] } }, @@ -109,7 +121,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/check-tsconfig-type-surface.cjs && 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 && node ./scripts/check-export-coverage.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/check-export-coverage.cjs b/packages/superdoc/scripts/check-export-coverage.cjs new file mode 100644 index 0000000000..8e822a58a1 --- /dev/null +++ b/packages/superdoc/scripts/check-export-coverage.cjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node +/** + * SD-2953: enforce that every `package.json` `exports` entry carries + * type information consumers can resolve. Pre-SD-2953 the `./converter`, + * `./docx-zipper`, and `./file-zipper` subpaths were exported at runtime + * but had no `types` field, leaving strict TypeScript consumers with + * TS7016. This audit prevents that regression class from reappearing. + * + * For each `exports` entry, one of these must hold: + * 1. The entry has a `types` field whose target file exists. + * 2. The entry resolves to an asset file (currently `.css` only). + * 3. The entry is on `RUNTIME_ONLY_ALLOWLIST` with a documented reason. + * + * Anything else fails the build. + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const packageRoot = path.resolve(__dirname, '..'); +const packageJson = require(path.join(packageRoot, 'package.json')); + +// Subpaths that are deliberately runtime-only with no type contract. +// Each entry must have a documented reason. Empty today; SD-2953 added +// types for the three previously-tolerated runtime-only legacy paths. +const RUNTIME_ONLY_ALLOWLIST = {}; + +const ASSET_EXTENSIONS = new Set(['.css']); + +function entryAllowlistedAsset(value) { + if (typeof value === 'string') return ASSET_EXTENSIONS.has(path.extname(value)); + return false; +} + +function entryHasTypes(value) { + if (typeof value === 'string') return false; + if (typeof value !== 'object' || value === null) return false; + return typeof value.types === 'string'; +} + +function typesTargetExists(value) { + if (!entryHasTypes(value)) return false; + return fs.existsSync(path.resolve(packageRoot, value.types)); +} + +const violations = []; +for (const [subpath, value] of Object.entries(packageJson.exports || {})) { + if (subpath === '.') continue; // top-level types are checked via the package.json `types` field + if (entryAllowlistedAsset(value)) continue; + if (RUNTIME_ONLY_ALLOWLIST[subpath]) continue; + + if (!entryHasTypes(value)) { + violations.push({ subpath, reason: 'missing `types` field in conditional exports' }); + continue; + } + if (!typesTargetExists(value)) { + violations.push({ subpath, reason: `\`types\` target does not exist: ${value.types}` }); + } +} + +if (violations.length > 0) { + console.error('[check-export-coverage] package.json exports without resolvable types:'); + for (const { subpath, reason } of violations) { + console.error(` - ${subpath}: ${reason}`); + } + console.error('Add a `types` field, classify as an asset, or add to RUNTIME_ONLY_ALLOWLIST with a reason.'); + process.exit(1); +} + +const totalChecked = Object.keys(packageJson.exports || {}).length; +console.log(`[check-export-coverage] ✓ ${totalChecked} exports entries all carry resolvable types or asset/legacy classification`); diff --git a/tests/consumer-typecheck/src/imports-converter.ts b/tests/consumer-typecheck/src/imports-converter.ts new file mode 100644 index 0000000000..2c2196e0c4 --- /dev/null +++ b/tests/consumer-typecheck/src/imports-converter.ts @@ -0,0 +1,20 @@ +/** + * Consumer typecheck: superdoc/converter subpath. + * + * Pre-SD-2953 this subpath was exported at runtime but had no `.d.ts`, + * so a strict consumer importing from it hit TS7016. SD-2953 added a + * `types` field pointing at the existing SuperConverter declaration. + */ + +import { SuperConverter } from 'superdoc/converter'; + +type IsAny = 0 extends 1 & T ? true : false; +type Assert = T; + +// SuperConverter must NOT be `any` (the SD-2828 contract). +type _ConverterReal = Assert>; + +// Static methods documented in the .d.ts must resolve. +const _v: string | null = SuperConverter.extractDocumentGuid(''); + +void _v; diff --git a/tests/consumer-typecheck/src/imports-docx-zipper.ts b/tests/consumer-typecheck/src/imports-docx-zipper.ts new file mode 100644 index 0000000000..e692d26df1 --- /dev/null +++ b/tests/consumer-typecheck/src/imports-docx-zipper.ts @@ -0,0 +1,20 @@ +/** + * Consumer typecheck: superdoc/docx-zipper subpath. + * + * Pre-SD-2953 this subpath was exported at runtime but had no `.d.ts`, + * so a strict consumer importing from it hit TS7016. SD-2953 added a + * `types` field pointing at the existing DocxZipper declaration. + */ + +import DocxZipper from 'superdoc/docx-zipper'; + +type IsAny = 0 extends 1 & T ? true : false; +type Assert = T; + +// DocxZipper must NOT be `any`. +type _ZipperReal = Assert>; + +// Constructable as a class. +const _zipper = new DocxZipper(); + +void _zipper; diff --git a/tests/consumer-typecheck/src/imports-file-zipper.ts b/tests/consumer-typecheck/src/imports-file-zipper.ts new file mode 100644 index 0000000000..92c337f8b7 --- /dev/null +++ b/tests/consumer-typecheck/src/imports-file-zipper.ts @@ -0,0 +1,22 @@ +/** + * Consumer typecheck: superdoc/file-zipper subpath. + * + * Pre-SD-2953 this subpath was exported at runtime but had no `.d.ts`, + * so a strict consumer importing from it hit TS7016. SD-2953 added a + * `types` field pointing at the existing zipper.js declaration. + */ + +import { createZip } from 'superdoc/file-zipper'; + +type IsAny = 0 extends 1 & T ? true : false; +type Assert = T; + +// createZip must NOT be `any`. +type _CreateZipReal = Assert>; + +// Returns a Promise per the JSDoc. +declare const blobs: any; +declare const fileNames: any; +const _result: Promise = createZip(blobs, fileNames); + +void _result; diff --git a/tests/consumer-typecheck/typecheck-matrix.mjs b/tests/consumer-typecheck/typecheck-matrix.mjs index b46a673a33..a93690b784 100644 --- a/tests/consumer-typecheck/typecheck-matrix.mjs +++ b/tests/consumer-typecheck/typecheck-matrix.mjs @@ -606,6 +606,64 @@ const scenarios = [ files: ['src/export-params-fields-highlight.ts'], mustPass: true, }, + // SD-2953: runtime-only subpaths (./converter, ./docx-zipper, ./file-zipper) + // were exported in package.json but lacked `types` fields, leaving strict + // consumers with TS7016. Each fixture imports through the public subpath + // and asserts the type resolves to a real declaration. + { + name: 'bundler / converter subpath (SD-2953)', + module: 'ESNext', + moduleResolution: 'bundler', + skipLibCheck: false, + strict: true, + files: ['src/imports-converter.ts'], + mustPass: true, + }, + { + name: 'node16 / converter subpath (SD-2953)', + module: 'Node16', + moduleResolution: 'node16', + skipLibCheck: false, + strict: true, + files: ['src/imports-converter.ts'], + mustPass: true, + }, + { + name: 'bundler / docx-zipper subpath (SD-2953)', + module: 'ESNext', + moduleResolution: 'bundler', + skipLibCheck: false, + strict: true, + files: ['src/imports-docx-zipper.ts'], + mustPass: true, + }, + { + name: 'node16 / docx-zipper subpath (SD-2953)', + module: 'Node16', + moduleResolution: 'node16', + skipLibCheck: false, + strict: true, + files: ['src/imports-docx-zipper.ts'], + mustPass: true, + }, + { + name: 'bundler / file-zipper subpath (SD-2953)', + module: 'ESNext', + moduleResolution: 'bundler', + skipLibCheck: false, + strict: true, + files: ['src/imports-file-zipper.ts'], + mustPass: true, + }, + { + name: 'node16 / file-zipper subpath (SD-2953)', + module: 'Node16', + moduleResolution: 'node16', + skipLibCheck: false, + strict: true, + files: ['src/imports-file-zipper.ts'], + mustPass: true, + }, ]; const tscPath = join(__dirname, 'node_modules', '.bin', 'tsc'); From ae237026ce65d27396cd9eb45d39e86b0f288d77 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 5 May 2026 13:12:07 -0300 Subject: [PATCH 2/2] docs(types): update Decision 4 prose to match SD-2953 outcome The PR's first commit updated the inventory table but missed the Decision 4 body, which still read "exported as runtime-only entries today". Treat as stale comment per CLAUDE.md. Update the prose to describe the post-SD-2953 state: the three subpaths now carry types, matrix fixtures lock the contract in, and the export-coverage audit prevents the gap class from reappearing. --- docs/architecture/package-boundaries.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/architecture/package-boundaries.md b/docs/architecture/package-boundaries.md index a99eeb22a1..804e6263e0 100644 --- a/docs/architecture/package-boundaries.md +++ b/docs/architecture/package-boundaries.md @@ -178,11 +178,11 @@ The relocation pattern is what `superdoc` currently uses for several internal-bu **Decision.** Keep as-is. The audit gate (SD-2832) plus the type ownership rules remove the customer-visible cost of the split. Restructuring without a strong forcing function is scope creep. Revisit only if the audit gate proves expensive to maintain because of the package count. -### Decision 4. Runtime-only `superdoc` subpaths are legacy public compatibility surface. +### Decision 4. Legacy public-compatibility `superdoc` subpaths. -**Context.** `./converter`, `./docx-zipper`, `./file-zipper` are exported as runtime-only entries today. The functionality they expose (DOCX conversion, zipping) is also reachable through `superdoc`'s main entry: `Editor.open`, `Editor.loadXmlData`, `SuperConverter`, `DocxZipper`, `createZip` are all exported from `superdoc`. +**Context.** `./converter`, `./docx-zipper`, `./file-zipper` are exported subpaths whose functionality (DOCX conversion, zipping) is also reachable through `superdoc`'s main entry: `Editor.open`, `Editor.loadXmlData`, `SuperConverter`, `DocxZipper`, `createZip` are all exported from `superdoc`. -**Decision.** All three subpaths are classified as **legacy public compatibility surface**. Migration target is `superdoc` itself (the symbols already exist there). We keep them exported, stop advertising them, point new use at `superdoc`, and add minimal type coverage so strict-mode TS does not break. +**Decision.** All three subpaths are classified as **legacy public compatibility surface**. Migration target is `superdoc` itself (the symbols already exist there). We keep them exported, stop advertising them, and point new use at `superdoc`. SD-2953 added `types` fields and matrix fixtures so strict-mode consumers no longer hit TS7016; the export-coverage audit (`check-export-coverage.cjs`) now enforces that every `package.json` exports entry carries types, an asset classification, or a documented runtime-only allowlist entry. ## Deliverables