Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/architecture/package-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
14 changes: 13 additions & 1 deletion packages/superdoc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
]
}
},
Expand All @@ -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",
Expand Down
71 changes: 71 additions & 0 deletions packages/superdoc/scripts/check-export-coverage.cjs
Original file line number Diff line number Diff line change
@@ -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`);
20 changes: 20 additions & 0 deletions tests/consumer-typecheck/src/imports-converter.ts
Original file line number Diff line number Diff line change
@@ -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<T> = 0 extends 1 & T ? true : false;
type Assert<T extends false> = T;

// SuperConverter must NOT be `any` (the SD-2828 contract).
type _ConverterReal = Assert<IsAny<typeof SuperConverter>>;

// Static methods documented in the .d.ts must resolve.
const _v: string | null = SuperConverter.extractDocumentGuid('<xml/>');

void _v;
20 changes: 20 additions & 0 deletions tests/consumer-typecheck/src/imports-docx-zipper.ts
Original file line number Diff line number Diff line change
@@ -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<T> = 0 extends 1 & T ? true : false;
type Assert<T extends false> = T;

// DocxZipper must NOT be `any`.
type _ZipperReal = Assert<IsAny<typeof DocxZipper>>;

// Constructable as a class.
const _zipper = new DocxZipper();

void _zipper;
22 changes: 22 additions & 0 deletions tests/consumer-typecheck/src/imports-file-zipper.ts
Original file line number Diff line number Diff line change
@@ -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<T> = 0 extends 1 & T ? true : false;
type Assert<T extends false> = T;

// createZip must NOT be `any`.
type _CreateZipReal = Assert<IsAny<typeof createZip>>;

// Returns a Promise<Blob> per the JSDoc.
declare const blobs: any;
declare const fileNames: any;
const _result: Promise<Blob> = createZip(blobs, fileNames);

void _result;
58 changes: 58 additions & 0 deletions tests/consumer-typecheck/typecheck-matrix.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading