Skip to content

Commit 3e09cbe

Browse files
authored
fix(superdoc): hide internal fields at source + repair node16 Config resolution (SD-2886) (#3056)
After SD-2880 published Config and SuperDocLayoutEngineOptions, two fields the source explicitly marks as internal became reachable on the public surface: - Config.socket. 'Internal: ... do not pass from outside.' Set by SuperDoc when modules.collaboration.providerType is 'hocuspocus'; a consumer who passes it from outside breaks the collaboration setup. - SuperDocLayoutEngineOptions.semanticOptions. 'Internal-only ... intentionally not a stable public API in v1.' A consumer who pins a shape couples themselves to behavior we may break. Earlier attempts and why they were wrong: 1. stripInternal: true in superdoc's tsconfig. Review pass found this would silently strip 100+ pre-existing @internal-tagged fields across super-editor (ImageAttrs.id, ParagraphAttrs.paraId, TableAttrs.sdBlockId, etc.) — a worse regression than the one this PR closes. 2. Omit<...> at the typedef alias boundary in superdoc/src/index.js. Review pass found this only narrowed the standalone Config alias. The nested Config.layoutEngineOptions still reached the un-Omitted SuperDocLayoutEngineOptions, and the SuperDoc constructor signature still took the un-Omitted Config — so `new SuperDoc({ socket })` and `{ layoutEngineOptions: { semanticOptions } }` still type- checked. This patch removes both fields from the source `Config` and `SuperDocLayoutEngineOptions` interfaces in core/types/index.ts and adds InternalConfig / InternalSuperDocLayoutEngineOptions extensions for the runtime callsites that need the augmented shape (SuperDoc.js's this.config.socket assignments, etc.). Public surface no longer carries either field anywhere it can be reached. Side fix surfaced by the same review: the JSDoc `@typedef {import('./types').X}` form in core/SuperDoc.js and core/surface-manager.js did not resolve under TypeScript's node16 module resolution. The published constructor signature decayed to `any` for node16 consumers, and the constructor probe (`new SuperDoc({ socket: undefined })`) silently type-checked. Switched all 11 `import('./types')` usages to `import('./types/index.js')` so the path resolves under both bundler and node16. The regression-net fixture under tests/consumer-typecheck/ now covers all four reachable surfaces: standalone Config alias, standalone SuperDocLayoutEngineOptions alias, nested Config.layoutEngineOptions, and the SuperDoc constructor parameter. Each uses @ts-expect-error so a regression on any path fails CI with TS2578 ("Unused @ts-expect-error directive"). Verified: matrix passes 31/31; declaration audit reports no FAIL findings; published .d.ts no longer references socket or semanticOptions on any reachable public path.
1 parent 2cbbd23 commit 3e09cbe

5 files changed

Lines changed: 127 additions & 28 deletions

File tree

packages/superdoc/src/core/SuperDoc.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,17 @@ const DEFAULT_AWARENESS_PALETTE = Object.freeze([
5757
'#F39C12',
5858
]);
5959

60-
/** @typedef {import('./types').User} User */
61-
/** @typedef {import('./types').Document} Document */
62-
/** @typedef {import('./types').Modules} Modules */
63-
/** @typedef {import('./types').Editor} Editor */
64-
/** @typedef {import('./types').DocumentMode} DocumentMode */
65-
/** @typedef {import('./types').Config} Config */
66-
/** @typedef {import('./types').ExportParams} ExportParams */
67-
/** @typedef {import('./types').UpgradeToCollaborationOptions} UpgradeToCollaborationOptions */
68-
/** @typedef {import('./types').SurfaceRequest} SurfaceRequest */
69-
/** @typedef {import('./types').SurfaceHandle} SurfaceHandle */
70-
/** @typedef {import('./types').NavigableAddress} NavigableAddress */
60+
/** @typedef {import('./types/index.js').User} User */
61+
/** @typedef {import('./types/index.js').Document} Document */
62+
/** @typedef {import('./types/index.js').Modules} Modules */
63+
/** @typedef {import('./types/index.js').Editor} Editor */
64+
/** @typedef {import('./types/index.js').DocumentMode} DocumentMode */
65+
/** @typedef {import('./types/index.js').Config} Config */
66+
/** @typedef {import('./types/index.js').ExportParams} ExportParams */
67+
/** @typedef {import('./types/index.js').UpgradeToCollaborationOptions} UpgradeToCollaborationOptions */
68+
/** @typedef {import('./types/index.js').SurfaceRequest} SurfaceRequest */
69+
/** @typedef {import('./types/index.js').SurfaceHandle} SurfaceHandle */
70+
/** @typedef {import('./types/index.js').NavigableAddress} NavigableAddress */
7171

7272
/**
7373
* SuperDoc class
@@ -585,7 +585,7 @@ export class SuperDoc extends EventEmitter {
585585
* or explicitly after this call during construction.
586586
*
587587
* @param {import('yjs').Doc} ydoc
588-
* @param {import('./types').CollaborationProvider} provider
588+
* @param {import('./types/index.js').CollaborationProvider} provider
589589
*/
590590
#attachExternalCollaboration(ydoc, provider) {
591591
this.isCollaborative = true;
@@ -750,7 +750,7 @@ export class SuperDoc extends EventEmitter {
750750
* underlying ref objects.
751751
*
752752
* @param {import('yjs').Doc | null} ydoc
753-
* @param {import('./types').CollaborationProvider | null} provider
753+
* @param {import('./types/index.js').CollaborationProvider | null} provider
754754
*/
755755
#setStoreDocumentCollaboration(ydoc, provider) {
756756
const storeDocs = this.superdocStore?.documents;
@@ -862,7 +862,7 @@ export class SuperDoc extends EventEmitter {
862862
* provider that exposes on/off but never emits sync cannot hang forever.
863863
* destroy() can abort this wait early via #abortUpgrade.
864864
*
865-
* @param {import('./types').CollaborationProvider} provider
865+
* @param {import('./types/index.js').CollaborationProvider} provider
866866
* @returns {Promise<void>}
867867
*/
868868
#waitForProviderSync(provider) {

packages/superdoc/src/core/surface-manager.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { shallowRef } from 'vue';
22

3-
/** @typedef {import('./types').SurfaceMode} SurfaceMode */
4-
/** @typedef {import('./types').SurfaceRequest} SurfaceRequest */
5-
/** @typedef {import('./types').SurfaceResolution} SurfaceResolution */
6-
/** @typedef {import('./types').SurfaceHandle} SurfaceHandle */
7-
/** @typedef {import('./types').SurfaceOutcome} SurfaceOutcome */
8-
/** @typedef {import('./types').SurfacesModuleConfig} SurfacesModuleConfig */
9-
/** @typedef {import('./types').ExternalSurfaceRenderContext} ExternalSurfaceRenderContext */
3+
/** @typedef {import('./types/index.js').SurfaceMode} SurfaceMode */
4+
/** @typedef {import('./types/index.js').SurfaceRequest} SurfaceRequest */
5+
/** @typedef {import('./types/index.js').SurfaceResolution} SurfaceResolution */
6+
/** @typedef {import('./types/index.js').SurfaceHandle} SurfaceHandle */
7+
/** @typedef {import('./types/index.js').SurfaceOutcome} SurfaceOutcome */
8+
/** @typedef {import('./types/index.js').SurfacesModuleConfig} SurfacesModuleConfig */
9+
/** @typedef {import('./types/index.js').ExternalSurfaceRenderContext} ExternalSurfaceRenderContext */
1010

1111
/**
1212
* @typedef {Object} ActiveSurface

packages/superdoc/src/core/types/index.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,11 +1086,6 @@ export interface SuperDocLayoutEngineOptions {
10861086
* - 'semantic': continuous semantic flow without visible pagination boundaries
10871087
*/
10881088
flowMode?: 'paginated' | 'semantic';
1089-
/**
1090-
* Internal-only semantic mode tuning options. This shape is intentionally
1091-
* not a stable public API in v1.
1092-
*/
1093-
semanticOptions?: object;
10941089
/**
10951090
* Deprecated. Use `modules.trackChanges` instead. Optional override for
10961091
* paginated track-changes rendering (e.g., `{ mode: 'original' }` or
@@ -1313,14 +1308,41 @@ export interface Config {
13131308
* back, warn, or block printing on unsupported faces.
13141309
*/
13151310
onFontsResolved?: (payload: FontsResolvedPayload) => void;
1311+
}
1312+
1313+
/**
1314+
* Internal augmentation of `Config` for runtime-only fields that must not
1315+
* appear on the published consumer surface. The `Config` interface above is
1316+
* the public contract; this type adds the fields SuperDoc sets/reads
1317+
* internally so the implementation can be type-checked without leaking the
1318+
* fields into customer IDE autocomplete.
1319+
*
1320+
* Use this from internal SuperDoc.js callsites that need the augmented shape
1321+
* (e.g. `/** @type {InternalConfig} *\/ (this.config).socket = ...`).
1322+
*/
1323+
export interface InternalConfig extends Config {
13161324
/**
1317-
* Internal: the shared websocket instance created by SuperDoc when
1325+
* The shared websocket instance created by SuperDoc when
13181326
* `modules.collaboration.providerType === 'hocuspocus'`. Set automatically;
1319-
* do not pass from outside.
1327+
* not part of the public Config surface.
13201328
*/
13211329
socket?: HocuspocusProviderWebsocket;
13221330
}
13231331

1332+
/**
1333+
* Internal augmentation of `SuperDocLayoutEngineOptions` for unstable tuning
1334+
* fields. The public `SuperDocLayoutEngineOptions` interface above is the
1335+
* customer-facing contract; this type adds fields the implementation may
1336+
* read but that are intentionally not part of the v1 stable API.
1337+
*/
1338+
export interface InternalSuperDocLayoutEngineOptions extends SuperDocLayoutEngineOptions {
1339+
/**
1340+
* Internal-only semantic mode tuning options. Shape may change without
1341+
* notice; not part of the public surface.
1342+
*/
1343+
semanticOptions?: object;
1344+
}
1345+
13241346
export type ProofingStatus = 'idle' | 'checking' | 'disabled' | 'degraded';
13251347

13261348
export interface ProofingError {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Consumer typecheck: internal-only fields must not appear on the
3+
* published Config / SuperDocLayoutEngineOptions surface (SD-2886).
4+
*
5+
* Two fields in `core/types/index.ts` are explicitly internal:
6+
*
7+
* - Config.socket. "Internal: ... do not pass from outside" (set
8+
* automatically when modules.collaboration.providerType === 'hocuspocus').
9+
* - SuperDocLayoutEngineOptions.semanticOptions. "Internal-only ...
10+
* intentionally not a stable public API in v1."
11+
*
12+
* Both are kept off the public surface by removing them from the source
13+
* `Config` / `SuperDocLayoutEngineOptions` interfaces in
14+
* `packages/superdoc/src/core/types/index.ts`. Internal callsites that need
15+
* the augmented shape use the `InternalConfig` /
16+
* `InternalSuperDocLayoutEngineOptions` extensions instead.
17+
*
18+
* The fixture pins both the standalone alias surface AND the nested /
19+
* constructor surface that the reviewer caught leaking past an earlier
20+
* Omit-at-the-boundary attempt. If a future change reintroduces either
21+
* field on the public types, an @ts-expect-error directive becomes unused
22+
* and tsc fails with TS2578 ("Unused @ts-expect-error directive").
23+
*/
24+
import { SuperDoc } from 'superdoc';
25+
import type { Config, SuperDocLayoutEngineOptions } from 'superdoc';
26+
27+
declare const config: Config;
28+
declare const layoutOpts: SuperDocLayoutEngineOptions;
29+
30+
// 1. Top-level Config alias must not expose `socket`.
31+
// @ts-expect-error - `socket` is internal-only and must not appear on the
32+
// published Config surface.
33+
void config.socket;
34+
35+
// 2. Top-level SuperDocLayoutEngineOptions alias must not expose
36+
// `semanticOptions`.
37+
// @ts-expect-error - `semanticOptions` is internal-only and must not appear
38+
// on the published SuperDocLayoutEngineOptions surface.
39+
void layoutOpts.semanticOptions;
40+
41+
// 3. Nested path: `Config.layoutEngineOptions` must reach the
42+
// public-surface SuperDocLayoutEngineOptions, not an internal variant. An
43+
// earlier Omit-at-the-boundary attempt fixed (1) and (2) but left this
44+
// nested reference resolving to the un-stripped source.
45+
// @ts-expect-error - `semanticOptions` must not be reachable through
46+
// `Config.layoutEngineOptions` either.
47+
void config.layoutEngineOptions?.semanticOptions;
48+
49+
// 4. Constructor path: `new SuperDoc({ socket })` must not type-check. The
50+
// constructor signature published in SuperDoc.d.ts must reach the same
51+
// public-surface Config the standalone alias points at.
52+
// @ts-expect-error - `socket` is not part of the public Config and must
53+
// not be assignable through the SuperDoc constructor.
54+
new SuperDoc({ selector: '#x', socket: undefined });

tests/consumer-typecheck/typecheck-matrix.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,29 @@ const scenarios = [
402402
files: ['src/modules-config-passthrough.ts'],
403403
mustPass: true,
404404
},
405+
// SD-2886: internal-only fields on Config / SuperDocLayoutEngineOptions
406+
// must not appear on the published surface. They are hidden via
407+
// `Omit<...>` re-exports in `packages/superdoc/src/index.js`. The fixture
408+
// relies on `@ts-expect-error` markers that stop erroring (TS2578) if a
409+
// future change leaks an internal field back onto the public surface.
410+
{
411+
name: 'bundler / internal fields stripped (SD-2886)',
412+
module: 'ESNext',
413+
moduleResolution: 'bundler',
414+
skipLibCheck: true,
415+
strict: true,
416+
files: ['src/internal-fields-stripped.ts'],
417+
mustPass: true,
418+
},
419+
{
420+
name: 'node16 / internal fields stripped (SD-2886)',
421+
module: 'Node16',
422+
moduleResolution: 'node16',
423+
skipLibCheck: true,
424+
strict: true,
425+
files: ['src/internal-fields-stripped.ts'],
426+
mustPass: true,
427+
},
405428
];
406429

407430
const tscPath = join(__dirname, 'node_modules', '.bin', 'tsc');

0 commit comments

Comments
 (0)