Skip to content

Commit b94c46b

Browse files
authored
fix(types): guard pm-adapter barrel relocation (#3144)
* fix(types): avoid missing pm-adapter barrel relocation * chore(types): clarify relocation guard naming
1 parent 9af59fa commit b94c46b

2 files changed

Lines changed: 79 additions & 29 deletions

File tree

packages/superdoc/scripts/audit-declarations.cjs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,13 @@ if (!fs.existsSync(distRoot)) {
5858
process.exit(1);
5959
}
6060

61-
// Packages whose types have been relocated into `superdoc`'s published
62-
// declaration tree. They must NEVER appear as a `declare module` block in
61+
// Packages whose public type dependencies are relocated into `superdoc`'s
62+
// published declaration tree or explicitly guarded from falling back to an
63+
// ambient shim. They must NEVER appear as a `declare module` block in
6364
// `_internal-shims.d.ts` — if they do, their types collapse to `any` for
64-
// consumers and we have a regression. Mirror of SD-2842's `RELOCATION_RULES`
65-
// in `ensure-types.cjs`; keep the two lists in sync.
66-
const RELOCATED_PACKAGES = [
65+
// consumers and we have a regression. Mirror of SD-2842's
66+
// `RELOCATION_GUARD_PACKAGES` in `ensure-types.cjs`; keep the two lists in sync.
67+
const RELOCATION_GUARD_PACKAGES = [
6768
'@superdoc/document-api',
6869
'@superdoc/contracts',
6970
'@superdoc/dom-contract',
@@ -167,7 +168,7 @@ const totalPnpmOccurrences = [...pnpmPathFindings.values()].reduce(
167168
0,
168169
);
169170

170-
const relocatedInShim = RELOCATED_PACKAGES.filter((pkg) =>
171+
const relocatedInShim = RELOCATION_GUARD_PACKAGES.filter((pkg) =>
171172
[...shimmedModules].some((mod) => mod === pkg || mod.startsWith(pkg + '/')),
172173
);
173174

@@ -205,7 +206,7 @@ if (relocatedInShim.length > 0) {
205206
console.log(`FAIL Relocated packages reappeared in _internal-shims.d.ts: ${relocatedInShim.join(', ')}`);
206207
console.log(' These packages have dedicated relocation rules in ensure-types.cjs and must not fall back to ambient any shims.');
207208
} else {
208-
console.log(`OK Relocated packages do not appear in shim file (${RELOCATED_PACKAGES.length} guarded)`);
209+
console.log(`OK Relocated packages do not appear in shim file (${RELOCATION_GUARD_PACKAGES.length} guarded)`);
209210
}
210211

211212
// Informational: remaining shimmed modules

packages/superdoc/scripts/ensure-types.cjs

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ const BAD_SUBPATH_RE = /(['"])([^'"]*\/index\.(?:js|ts))(\/[^'"]+)\1/g;
162162
let fixedFiles = 0;
163163
let totalReplacements = 0;
164164

165+
function escapeRegExp(value) {
166+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
167+
}
168+
165169
function appendJsExtensionToRelativeSpecifier(specifier, filePath) {
166170
if (!specifier.startsWith('./') && !specifier.startsWith('../')) return specifier;
167171
if (specifier.includes('?') || specifier.includes('#')) return specifier;
@@ -207,34 +211,62 @@ function rewriteDocApiPaths(fileContent, filePath) {
207211
// paths the consumer can resolve.
208212
//
209213
// SD-2893 note for pm-adapter: only specific type subpaths are
210-
// relocated (see vite.config.js include list). A bare `@superdoc/pm-adapter`
211-
// specifier would rewrite to a relative path that does not exist in dist.
212-
// The audit gate (RELOCATED_PACKAGES in audit-declarations.cjs) rejects
213-
// any unrewritten bare specifier at build time, so this is a build-time
214-
// failure rather than a silent consumer break. If a future public type
215-
// genuinely needs the pm-adapter barrel, widen the vite include and the
216-
// shim drain in lockstep.
214+
// relocated (see vite.config.js include list). Do not add a broad
215+
// `@superdoc/pm-adapter` rule unless the barrel declaration is also
216+
// emitted; otherwise a bare specifier would rewrite to a missing
217+
// relative path and evade the audit gate.
217218
const RELOCATION_RULES = [
218-
{ pkg: '@superdoc/contracts', distEntry: 'layout-engine/contracts/src/index.d.ts' },
219-
{ pkg: '@superdoc/dom-contract', distEntry: 'layout-engine/dom-contract/src/index.d.ts' },
220-
{ pkg: '@superdoc/layout-bridge', distEntry: 'layout-engine/layout-bridge/src/index.d.ts' },
221-
{ pkg: '@superdoc/painter-dom', distEntry: 'layout-engine/painters/dom/src/index.d.ts' },
222-
{ pkg: '@superdoc/pm-adapter', distEntry: 'layout-engine/pm-adapter/src/index.d.ts' },
219+
{ pkg: '@superdoc/contracts', distEntry: 'layout-engine/contracts/src/index.d.ts', matchSubpaths: true },
220+
{ pkg: '@superdoc/dom-contract', distEntry: 'layout-engine/dom-contract/src/index.d.ts', matchSubpaths: true },
221+
{ pkg: '@superdoc/layout-bridge', distEntry: 'layout-engine/layout-bridge/src/index.d.ts', matchSubpaths: true },
222+
{ pkg: '@superdoc/painter-dom', distEntry: 'layout-engine/painters/dom/src/index.d.ts', matchSubpaths: true },
223+
{
224+
pkg: '@superdoc/pm-adapter/converter-context.js',
225+
distEntry: 'layout-engine/pm-adapter/src/converter-context.d.ts',
226+
matchSubpaths: false,
227+
},
228+
{
229+
pkg: '@superdoc/pm-adapter/sections/types.js',
230+
distEntry: 'layout-engine/pm-adapter/src/sections/types.d.ts',
231+
matchSubpaths: false,
232+
},
233+
];
234+
235+
// Guard packages that must never fall back to `_internal-shims.d.ts`.
236+
// `@superdoc/pm-adapter` is guarded as a root package even though only
237+
// two exact subpaths are relocated today; a future bare-barrel leak should
238+
// fail the build rather than ship as `any`.
239+
const RELOCATION_GUARD_PACKAGES = [
240+
'@superdoc/document-api',
241+
'@superdoc/contracts',
242+
'@superdoc/dom-contract',
243+
'@superdoc/layout-bridge',
244+
'@superdoc/painter-dom',
245+
'@superdoc/pm-adapter',
223246
];
224247

225-
function makeRelocationRewriter({ pkg, distEntry }) {
248+
function isRelocatedSpecifier(mod) {
249+
return RELOCATION_RULES.some((rule) =>
250+
rule.matchSubpaths
251+
? mod === rule.pkg || mod.startsWith(rule.pkg + '/')
252+
: mod === rule.pkg,
253+
);
254+
}
255+
256+
function makeRelocationRewriter({ pkg, distEntry, matchSubpaths }) {
226257
// Match the package name with optional subpath, e.g. `@superdoc/contracts` or
227258
// `@superdoc/contracts/engines/tabs.js`. Anchored to either side of the
228259
// package segment so `@superdoc/contracts-something` is not matched.
229-
const escaped = pkg.replace(/\//g, '\\/');
230-
const re = new RegExp(`(['"])${escaped}(\\/[^'"]+)?\\1`, 'g');
260+
const escaped = escapeRegExp(pkg);
261+
const subpathPattern = matchSubpaths ? `(\\/[^'"]+)?` : '';
262+
const re = new RegExp(`(['"])${escaped}${subpathPattern}\\1`, 'g');
231263
return (fileContent, filePath) => {
232264
return fileContent.replace(re, (_match, quote, subpath = '') => {
233265
const target = path.join(distRoot, distEntry);
234266
let rel = path.relative(path.dirname(filePath), target).split(path.sep).join('/');
235267
if (!rel.startsWith('.')) rel = './' + rel;
236268
rel = rel.replace(/\.d\.ts$/, '.js');
237-
if (subpath) rel = rel.replace(/\/index\.js$/, subpath);
269+
if (matchSubpaths && subpath) rel = rel.replace(/\/index\.js$/, subpath);
238270
return `${quote}${rel}${quote}`;
239271
});
240272
};
@@ -245,6 +277,23 @@ const RELOCATION_REWRITERS = RELOCATION_RULES.map((rule) => ({
245277
rewrite: makeRelocationRewriter(rule),
246278
}));
247279

280+
// Any root specifier added here should also be listed in
281+
// RELOCATION_GUARD_PACKAGES so it cannot fall back to an ambient `any`
282+
// shim after we intentionally skip shim generation.
283+
const UNSHIMMED_PRIVATE_SPECIFIERS = new Set([
284+
'@superdoc/pm-adapter',
285+
]);
286+
287+
function shouldSkipWorkspaceShim(mod) {
288+
return (
289+
mod.startsWith('.') ||
290+
mod.startsWith('@superdoc/super-editor') ||
291+
mod.startsWith('@superdoc/document-api') ||
292+
isRelocatedSpecifier(mod) ||
293+
UNSHIMMED_PRIVATE_SPECIFIERS.has(mod)
294+
);
295+
}
296+
248297
const dtsFiles = findDtsFiles(distRoot);
249298
for (const filePath of dtsFiles) {
250299
let fileContent = fs.readFileSync(filePath, 'utf8');
@@ -387,7 +436,7 @@ for (const filePath of dtsFiles) {
387436
const mod = m[2];
388437

389438
// Skip relative imports and already-handled packages
390-
if (mod.startsWith('.') || mod.startsWith('@superdoc/super-editor') || mod.startsWith('@superdoc/document-api') || RELOCATION_RULES.some((r) => mod === r.pkg || mod.startsWith(r.pkg + '/'))) continue;
439+
if (shouldSkipWorkspaceShim(mod)) continue;
391440

392441
if (mod.startsWith('@superdoc/')) {
393442
if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set());
@@ -400,7 +449,7 @@ for (const filePath of dtsFiles) {
400449
const dynamicImports = fileContent.matchAll(/import\(['"]([^'"]+)['"]\)\.(\w+)/g);
401450
for (const m of dynamicImports) {
402451
const mod = m[1];
403-
if (mod.startsWith('.') || mod.startsWith('@superdoc/super-editor') || mod.startsWith('@superdoc/document-api') || RELOCATION_RULES.some((r) => mod === r.pkg || mod.startsWith(r.pkg + '/'))) continue;
452+
if (shouldSkipWorkspaceShim(mod)) continue;
404453

405454
if (mod.startsWith('@superdoc/')) {
406455
if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set());
@@ -420,7 +469,7 @@ for (const filePath of dtsFiles) {
420469
// resolve through the shim and collapse internal-only types
421470
// (Comment, CommentContent, CommentJSON) to `any`. None of those
422471
// appear on superdoc's public surface, so the collapse is safe.
423-
if (mod.startsWith('@superdoc/super-editor') || mod.startsWith('@superdoc/document-api') || RELOCATION_RULES.some((r) => mod === r.pkg || mod.startsWith(r.pkg + '/'))) continue;
472+
if (shouldSkipWorkspaceShim(mod)) continue;
424473
if (!workspaceImports.has(mod)) workspaceImports.set(mod, new Set());
425474
}
426475
}
@@ -501,11 +550,11 @@ console.log(`[ensure-types] ✓ Generated ambient shims for ${wsCount} workspace
501550
// rewrite or include for that package and customers would see `any`
502551
// for those types again.
503552
const shimContent = fs.readFileSync(shimPath, 'utf8');
504-
const SHIM_FORBIDDEN = ['@superdoc/document-api', ...RELOCATION_RULES.map((r) => r.pkg)];
553+
const SHIM_FORBIDDEN = RELOCATION_GUARD_PACKAGES;
505554
for (const pkg of SHIM_FORBIDDEN) {
506-
const re = new RegExp(`declare module '${pkg.replace(/\//g, '\\/')}(\\/[^']+)?'`);
555+
const re = new RegExp(`declare module '${escapeRegExp(pkg)}(\\/[^']+)?'`);
507556
if (re.test(shimContent)) {
508-
console.error(`[ensure-types] ✗ ${pkg} appears in _internal-shims.d.ts. Its types should resolve via the relocation rewrite, not via an ambient any shim. Investigate the include glob, the rewrite rule, and the shim-skip predicate for this package.`);
557+
console.error(`[ensure-types] ✗ ${pkg} appears in _internal-shims.d.ts. Its types should resolve via a relocation rewrite or fail the audit as an unrelocated leak, not via an ambient any shim. Investigate the include glob, the rewrite rule, and the shim-skip predicate for this package.`);
509558
process.exit(1);
510559
}
511560
}

0 commit comments

Comments
 (0)