Skip to content
2 changes: 1 addition & 1 deletion packages/superdoc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,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/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",
"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
39 changes: 13 additions & 26 deletions packages/superdoc/scripts/audit-declarations.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
const fs = require('node:fs');
const path = require('node:path');

// SD-2864: canonical taxonomy for the published type surface. The lists
// below previously duplicated data from ensure-types.cjs; both now derive
// from the same config so the two scripts cannot drift.
const typeSurface = require('./type-surface.config.cjs');

const distRoot = path.resolve(__dirname, '..', 'dist');

// SD-2859: strict is the default. The audit fails the build on any
Expand All @@ -52,35 +57,17 @@ if (!fs.existsSync(distRoot)) {
process.exit(1);
}

// Packages whose public type dependencies are relocated into `superdoc`'s
// published declaration tree or explicitly guarded from falling back to an
// ambient shim. They must NEVER appear as a `declare module` block in
// `_internal-shims.d.ts` — if they do, their types collapse to `any` for
// consumers and we have a regression. Mirror of SD-2842's
// `RELOCATION_GUARD_PACKAGES` in `ensure-types.cjs`; keep the two lists in sync.
const RELOCATION_GUARD_PACKAGES = [
'@superdoc/document-api',
'@superdoc/contracts',
'@superdoc/dom-contract',
'@superdoc/layout-bridge',
'@superdoc/layout-engine',
'@superdoc/painter-dom',
'@superdoc/pm-adapter',
'@superdoc/style-engine',
'@superdoc/common',
'@superdoc/common/list-marker-utils',
];
// Packages that must NEVER appear as a `declare module` block in
// `_internal-shims.d.ts`. After SD-2942 the file is no longer emitted, so
// this list is a defense against stale tarballs and future re-introduction.
// Source: type-surface.config.cjs `relocationGuardPackages`.
const RELOCATION_GUARD_PACKAGES = typeSurface.relocationGuardPackages;

// Specifiers that may appear as bare imports in published d.ts files even
// though they are private workspace packages. Each entry has a documented
// reason; anything outside this allowlist (and outside the shim file) is a
// real leak per Rule 1.
const RULE1_ALLOWLIST = {
// Legacy public surface per the RFC. Resolves through `superdoc`'s
// published dist tree at runtime via the existing rewrite/include rules.
// Deep subpaths beyond the curated public surface are NOT allowlisted.
'@superdoc/super-editor': 'legacy public surface (RFC Decision 1)',
};
// reason; anything outside this allowlist is a real leak per Rule 1.
// Source: type-surface.config.cjs `rule1Allowlist`.
const RULE1_ALLOWLIST = typeSurface.rule1Allowlist;

function isRule1Allowed(specifier, shimmedSet) {
if (RULE1_ALLOWLIST[specifier]) return true;
Expand Down
68 changes: 68 additions & 0 deletions packages/superdoc/scripts/check-tsconfig-type-surface.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env node
/**
* SD-2864: enforce parity between tsconfig.json's `include` array and
* `type-surface.config.cjs`. tsconfig.json is the one consumer of the
* type-surface taxonomy that has no scripting layer (it's plain JSON),
* so we don't generate it; this check fails the build if the on-disk
* file drifts from the config in either direction.
*
* Expected shape: tsconfig.json's `include` MUST equal exactly the
* union of `baseTsconfigIncludes` (foundational sources) and
* `relocations[*].tsconfigIncludes` (per-relocation paths). No more,
* no less.
*
* Drift modes this catches:
* - A new relocation added to the config but not mirrored in
* tsconfig.json (typecheck for that source tree silently misses it).
* - A relocation removed from the config but its tsconfig.json entry
* left stale (entry would compile against source the type-surface
* no longer claims to manage; undermines single-source-of-truth).
* - Foundational base entries dropped from tsconfig.json by mistake.
*
* If foundational entries beyond the current three are needed (e.g. a
* future public package source root), add them to `baseTsconfigIncludes`
* in type-surface.config.cjs rather than carrying them only in
* tsconfig.json.
*/

const fs = require('node:fs');
const path = require('node:path');

const tsconfigPath = path.resolve(__dirname, '..', 'tsconfig.json');
const typeSurface = require('./type-surface.config.cjs');

const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf8'));
const tsconfigIncludes = new Set(tsconfig.include || []);

const relocationIncludes = typeSurface.relocations.flatMap((r) => r.tsconfigIncludes);
const expected = new Set([...typeSurface.baseTsconfigIncludes, ...relocationIncludes]);

const missing = [...expected].filter((entry) => !tsconfigIncludes.has(entry));
const stale = [...tsconfigIncludes].filter((entry) => !expected.has(entry));

let failed = false;

if (missing.length > 0) {
failed = true;
console.error('[check-tsconfig-type-surface] tsconfig.json `include` is missing entries required by type-surface.config.cjs:');
for (const entry of missing) {
const owner = typeSurface.relocations.find((r) => r.tsconfigIncludes.includes(entry));
const reason = owner ? `required by ${owner.pkg}` : 'foundational base include';
console.error(` - ${entry} (${reason})`);
}
}

if (stale.length > 0) {
failed = true;
console.error('[check-tsconfig-type-surface] tsconfig.json `include` has entries not declared in type-surface.config.cjs:');
for (const entry of stale) {
console.error(` - ${entry} (stale - remove from tsconfig.json or add to baseTsconfigIncludes / a relocation)`);
}
}

if (failed) {
console.error('Update packages/superdoc/tsconfig.json or packages/superdoc/scripts/type-surface.config.cjs so the two stay in sync.');
process.exit(1);
}

console.log(`[check-tsconfig-type-surface] ✓ tsconfig.json mirrors ${expected.size} type-surface include paths exactly`);
137 changes: 30 additions & 107 deletions packages/superdoc/scripts/ensure-types.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
const fs = require('node:fs');
const path = require('node:path');

// SD-2864: canonical taxonomy for the published type surface. Mirrors
// vite.config.js, tsconfig.json, and audit-declarations.cjs from a single
// data file so contributors only edit one place to add a new relocation.
const typeSurface = require('./type-surface.config.cjs');

// Verify that vite-plugin-dts generated the expected type entry points.
// Path aliases are resolved by vite-plugin-dts via tsconfig.json paths.
const distRoot = path.resolve(__dirname, '..', 'dist');
Expand All @@ -13,19 +18,9 @@ const repoRoot = path.resolve(__dirname, '..', '..', '..');
// `core-command-map.d.ts` is referenced via a relative import from another
// emitted `.d.ts`, the consumer hits an unresolved-module error. Copy
// every hand-written `.d.ts` from the source trees we publish into the
// matching dist location so those imports resolve.
// Hand-written `.d.ts` files we know are internal-only and must NOT ship
// in `superdoc`'s published dist. The copy step is opt-in via filename
// blocklist (rather than e.g. a per-file directive) so future hand-written
// declarations land in dist by default and the cost of skipping one is one
// line here. Each entry should have a comment explaining why.
const HANDWRITTEN_DTS_BLOCKLIST = new Set([
// Ambient module declarations for internal `@superdoc/super-editor/converter/internal/...`
// subpaths. Nothing in `superdoc`'s shipped surface actually imports those subpaths,
// so the declarations would only leak the bare specifiers into published d.ts.
// Keep the file in source for super-editor's own typecheck; just don't ship it. (SD-2859)
'converter-internal.d.ts',
]);
// matching dist location so those imports resolve. Source list:
// type-surface.config.cjs `handwrittenDtsBlocklist`.
const HANDWRITTEN_DTS_BLOCKLIST = new Set(typeSurface.handwrittenDtsBlocklist);

function copyHandwrittenDtsFiles(srcDir, destDir) {
let copied = 0;
Expand Down Expand Up @@ -66,11 +61,10 @@ if (handwrittenCopiedSuperEditor > 0) {
// public surface. Adding shared/ to vite-plugin-dts's `include` would shift the
// common-ancestor of all source files to the repo root and reorganise the
// entire dist tree, so we run tsc directly for just the files we relocate.
// Today: list-marker-utils plus its sibling layout-constants, and
// comments-types (the four Comment* types referenced via bare @superdoc/common
// imports in three internal-only dist d.ts files). Add new entries here in
// lockstep with `RELOCATION_RULES` below.
const SHARED_COMMON_DTS_TARGETS = ['list-marker-utils.ts', 'layout-constants.ts', 'comments-types.ts'];
// Source list: type-surface.config.cjs `sharedCommonDtsTargets`. Each entry
// pairs with a `relocations` rule whose distEntry points at
// `shared/common/<filename>.d.ts`.
const SHARED_COMMON_DTS_TARGETS = typeSurface.sharedCommonDtsTargets;
{
const { spawnSync: _spawnSync } = require('node:child_process');
const tscBin = path.join(repoRoot, 'node_modules', '.bin', 'tsc');
Expand Down Expand Up @@ -99,12 +93,7 @@ const SHARED_COMMON_DTS_TARGETS = ['list-marker-utils.ts', 'layout-constants.ts'
console.log(`[ensure-types] ✓ Emitted ${SHARED_COMMON_DTS_TARGETS.length} shared/common declarations`);
}

const requiredEntryPoints = [
'superdoc/src/index.d.ts',
'superdoc/src/super-editor.d.ts',
'super-editor/src/index.d.ts',
'super-editor/src/types.d.ts',
];
const requiredEntryPoints = typeSurface.requiredEntryPoints;

for (const entry of requiredEntryPoints) {
const fullPath = path.join(distRoot, entry);
Expand Down Expand Up @@ -243,84 +232,20 @@ function rewriteDocApiPaths(fileContent, filePath) {
});
}

// SD-2842: relocate workspace packages whose types appear on the
// public surface. Same idea as the document-api rewrite above: emit
// their declarations into superdoc's dist (via vite-plugin-dts include)
// and redirect bare specifiers in emitted .d.ts files to relative
// paths the consumer can resolve.
//
// SD-2893 note for pm-adapter: only specific type subpaths are
// relocated (see vite.config.js include list). Do not add a broad
// `@superdoc/pm-adapter` rule unless the barrel declaration is also
// emitted; otherwise a bare specifier would rewrite to a missing
// relative path and evade the audit gate.
const RELOCATION_RULES = [
{ pkg: '@superdoc/contracts', distEntry: 'layout-engine/contracts/src/index.d.ts', matchSubpaths: true },
{ pkg: '@superdoc/dom-contract', distEntry: 'layout-engine/dom-contract/src/index.d.ts', matchSubpaths: true },
{ pkg: '@superdoc/layout-bridge', distEntry: 'layout-engine/layout-bridge/src/index.d.ts', matchSubpaths: true },
{ pkg: '@superdoc/layout-engine', distEntry: 'layout-engine/layout-engine/src/index.d.ts', matchSubpaths: true },
{ pkg: '@superdoc/painter-dom', distEntry: 'layout-engine/painters/dom/src/index.d.ts', matchSubpaths: true },
{
pkg: '@superdoc/pm-adapter/converter-context.js',
distEntry: 'layout-engine/pm-adapter/src/converter-context.d.ts',
matchSubpaths: false,
},
{
pkg: '@superdoc/pm-adapter/sections/types.js',
distEntry: 'layout-engine/pm-adapter/src/sections/types.d.ts',
matchSubpaths: false,
},
// SD-2893: list-marker-utils is the only @superdoc/common subpath publicly
// reachable today (via painter-dom). Relocate just this file so the bare
// @superdoc/common shim does not capture it; the parent @superdoc/common
// package and other subpaths stay shimmed until separately drained.
{
pkg: '@superdoc/common/list-marker-utils',
distEntry: 'shared/common/list-marker-utils.d.ts',
matchSubpaths: false,
},
// SD-2893: only the /ooxml subpath of style-engine is publicly reachable.
// Relocate just this subpath plus its sibling cascade.ts dependency
// (see vite.config.js include list). The bare @superdoc/style-engine is
// guarded but unrewritten; if a future bare-barrel leak appears the audit
// gate fails rather than producing a missing relative path.
{
pkg: '@superdoc/style-engine/ooxml',
distEntry: 'layout-engine/style-engine/src/ooxml/index.d.ts',
matchSubpaths: false,
},
// SD-2893: bare @superdoc/common appears in three internal-only dist d.ts
// files for the four Comment* types (Comment, CommentContent, CommentJSON,
// CommentThreadingProfile). Point the bare specifier at comments-types.d.ts
// (emitted via SHARED_COMMON_DTS_TARGETS) so the rewrite resolves to a real
// file. matchSubpaths: false because only the bare specifier is referenced;
// any future @superdoc/common/<other-subpath> import would not be auto-
// rewritten, falling through to the audit gate. The runtime-value imports
// from the main entry (DOCX, PDF, HTML, getFileObject, compareVersions,
// BlankDOCX) are still handled by the inline-replacement step above.
{
pkg: '@superdoc/common',
distEntry: 'shared/common/comments-types.d.ts',
matchSubpaths: false,
},
];

// Guard packages that must never fall back to `_internal-shims.d.ts`.
// `@superdoc/pm-adapter` is guarded as a root package even though only
// two exact subpaths are relocated today; a future bare-barrel leak should
// fail the build rather than ship as `any`.
const RELOCATION_GUARD_PACKAGES = [
'@superdoc/document-api',
'@superdoc/contracts',
'@superdoc/dom-contract',
'@superdoc/layout-bridge',
'@superdoc/layout-engine',
'@superdoc/painter-dom',
'@superdoc/pm-adapter',
'@superdoc/style-engine',
'@superdoc/common',
'@superdoc/common/list-marker-utils',
];
// SD-2842 / SD-2864: relocate workspace packages whose types appear on the
// public surface. Each rule redirects bare/subpath specifiers in emitted
// .d.ts files to a relative path inside dist. The canonical list lives in
// type-surface.config.cjs; this script picks the fields it needs.
const RELOCATION_RULES = typeSurface.relocations.map(({ pkg, distEntry, matchSubpaths }) => ({
pkg,
distEntry,
matchSubpaths,
}));

// Guard packages that must never appear as a `declare module` block in
// `_internal-shims.d.ts`. SD-2942 removed the shim emit; this list is
// kept as defense against stale tarballs and future re-introduction.
const RELOCATION_GUARD_PACKAGES = typeSurface.relocationGuardPackages;

function isRelocatedSpecifier(mod) {
return RELOCATION_RULES.some((rule) =>
Expand Down Expand Up @@ -356,11 +281,9 @@ const RELOCATION_REWRITERS = RELOCATION_RULES.map((rule) => ({

// Any root specifier added here should also be listed in
// RELOCATION_GUARD_PACKAGES so it cannot fall back to an ambient `any`
// shim after we intentionally skip shim generation.
const UNSHIMMED_PRIVATE_SPECIFIERS = new Set([
'@superdoc/pm-adapter',
'@superdoc/style-engine',
]);
// shim after we intentionally skip shim generation. List source:
// type-surface.config.cjs (`unshimmedPrivateSpecifiers`).
const UNSHIMMED_PRIVATE_SPECIFIERS = new Set(typeSurface.unshimmedPrivateSpecifiers);

function shouldSkipWorkspaceShim(mod) {
return (
Expand Down
Loading
Loading