diff --git a/.changeset/6120-doc-snippet-dependency-resolution.md b/.changeset/6120-doc-snippet-dependency-resolution.md new file mode 100644 index 0000000000..01f3621a10 --- /dev/null +++ b/.changeset/6120-doc-snippet-dependency-resolution.md @@ -0,0 +1,31 @@ +--- +--- + +Doc-snippet gate tooling only, no published package source changed. + +`check-doc-snippet-types` compiles every covered snippet as its own module at the +repository ROOT. Workspace packages resolved there — it builds `paths` from each +package's own `exports` — but a THIRD-PARTY specifier did not: under pnpm, a +workspace package's own dependency is not hoisted to the root, so a snippet that +imports `lucide-react` failed `TS2307` even though `@object-ui/layout` and +`@object-ui/components` both declare it and any reader who installs those +packages gets it. Five correct blocks across `content/docs/layout` were red on +nothing but that, which blocked the whole group from being brought under the +gate. The snippets were right; the resolution environment was the gap. + +The gate now derives `paths` for the specifiers each imported package DECLARES in +its own `dependencies`, resolved from inside that package's directory — the +environment a real consumer has. Deliberately narrow, and it fails closed: +`dependencies` only (not peers, not devDependencies), only packages a covered +document actually imports, only the bare specifier (no subpath wildcard), and a +dependency shipping no types is left unresolvable rather than approximated. The +repository's own manifests are untouched — declaring `lucide-react` at the root +to buy a snippet its coverage would change what this repo claims to need in order +to satisfy a checker. + +A fourth self-control (`undeclared`) now runs on every invocation and keeps that +narrowness honest: a module importing `@floating-ui/react-dom` — installed here +as a transitive of Radix's popper, declared by no package a covered document +imports — MUST still produce `TS2307`. Widen resolution past the declarations and +that control goes green, which is the only way to notice that the gate has become +a rubber stamp no snippet can fail. diff --git a/scripts/__tests__/check-doc-snippet-types.test.ts b/scripts/__tests__/check-doc-snippet-types.test.ts index 4389443d98..571f20a73a 100644 --- a/scripts/__tests__/check-doc-snippet-types.test.ts +++ b/scripts/__tests__/check-doc-snippet-types.test.ts @@ -8,9 +8,12 @@ import { fileURLToPath } from 'node:url'; // `tsconfig.scripts.json` (`allowJs`), so no `@ts-expect-error` here. import { FRAGMENT_MARKER_EXAMPLES, + UNDECLARED_CONTROL_PACKAGE, UNGATED_DOCS, analyze, + deriveDeclaredDependencyPaths, derivePackageTypePaths, + findInstalledCopy, listDocuments, scanFences, } from '../check-doc-snippet-types.mjs'; @@ -35,6 +38,13 @@ import { * `tsconfig.json` maps the workspace to source; a harness that inherited it * would check the docs against code no consumer sees. * 5. **The gate is wired**, in a workflow a docs-only pull request can start. + * 6. **Third-party resolution reaches exactly as far as the imported packages + * DECLARE** (objectui#6120). This one's failure mode is the worst in the list + * because it is invisible: widen resolution past the declarations and every + * document stays green while the gate stops being able to fail. The suite + * therefore pins both directions — a declared dependency IS mapped, and an + * installed-but-undeclared one is NOT — plus the two preconditions the + * UNDECLARED control needs in order to mean anything. * * Fixtures are throwaway trees, never `content/docs`: a committed fixture page * would have to contain a deliberately broken snippet, and this very gate scans @@ -212,6 +222,147 @@ describe('this repository', () => { }); }); +describe('third-party resolution reaches exactly as far as the imported packages declare', () => { + /** A workspace package with its own `node_modules`, the way pnpm links one. */ + function treeWithDependency(files: Record = {}): string { + return tempTree({ + 'content/docs/a.mdx': [FENCE + 'ts', "import 'declared-dep';", FENCE].join('\n'), + 'packages/pkg-a/package.json': JSON.stringify({ + name: 'pkg-a', + dependencies: { 'declared-dep': '^1.0.0' }, + peerDependencies: { 'peer-dep': '^1.0.0' }, + devDependencies: { 'dev-dep': '^1.0.0' }, + }), + 'packages/pkg-a/node_modules/declared-dep/package.json': JSON.stringify({ + name: 'declared-dep', + types: 'index.d.ts', + }), + 'packages/pkg-a/node_modules/declared-dep/index.d.ts': 'export declare const declared: number;\n', + // Installed right beside it and NOT declared: the shape a blanket mapping + // over node_modules would pick up, and the one no consumer can import. + 'packages/pkg-a/node_modules/undeclared-dep/package.json': JSON.stringify({ + name: 'undeclared-dep', + types: 'index.d.ts', + }), + 'packages/pkg-a/node_modules/undeclared-dep/index.d.ts': 'export declare const undeclared: number;\n', + 'packages/pkg-a/node_modules/peer-dep/package.json': JSON.stringify({ name: 'peer-dep', types: 'index.d.ts' }), + 'packages/pkg-a/node_modules/peer-dep/index.d.ts': 'export declare const peer: number;\n', + 'packages/pkg-a/node_modules/dev-dep/package.json': JSON.stringify({ name: 'dev-dep', types: 'index.d.ts' }), + 'packages/pkg-a/node_modules/dev-dep/index.d.ts': 'export declare const dev: number;\n', + ...files, + }); + } + + const derive = (root: string, imported: string[] = ['pkg-a']) => + deriveDeclaredDependencyPaths(root, imported, { 'pkg-a': 'packages/pkg-a' }) as unknown as { + paths: Record; + declaredBy: Record; + untyped: { specifier: string }[]; + }; + + it('maps a specifier the imported package DECLARES — that is what a consumer resolves', () => { + const { paths, declaredBy } = derive(treeWithDependency()); + expect(Object.keys(paths)).toContain('declared-dep'); + expect(paths['declared-dep'][0]).toMatch(/declared-dep[\\/]index\.d\.ts$/); + expect(declaredBy['declared-dep']).toBe('pkg-a'); + }); + + it('does NOT map a package that is merely INSTALLED — the control that keeps this a check', () => { + // If this ever passes, resolution has been widened to a blanket mapping and + // a snippet may import what no reader of these packages can get. + const { paths } = derive(treeWithDependency()); + expect(Object.keys(paths)).not.toContain('undeclared-dep'); + }); + + it('does not map peerDependencies or devDependencies — it fails CLOSED', () => { + const { paths } = derive(treeWithDependency()); + expect(Object.keys(paths)).not.toContain('peer-dep'); + expect(Object.keys(paths)).not.toContain('dev-dep'); + }); + + it('maps nothing for a package no covered document imports', () => { + const { paths } = derive(treeWithDependency(), []); + expect(paths).toEqual({}); + }); + + it('leaves a specifier that ships no types unresolvable rather than approximating it', () => { + // A JS-only dependency: declared, installed, and carrying nothing a strict + // program can judge. Mapping it to something approximate would report green + // over a snippet nobody type-checked; leaving it unresolvable fails honestly. + const root = tempTree({ + 'packages/pkg-a/package.json': JSON.stringify({ + name: 'pkg-a', + dependencies: { 'untyped-dep': '^1.0.0' }, + }), + 'packages/pkg-a/node_modules/untyped-dep/package.json': JSON.stringify({ + name: 'untyped-dep', + main: 'index.js', + }), + 'packages/pkg-a/node_modules/untyped-dep/index.js': 'module.exports = {};\n', + }); + const { paths, untyped } = derive(root); + expect(Object.keys(paths)).not.toContain('untyped-dep'); + expect(untyped.map((u) => u.specifier)).toContain('untyped-dep'); + }); + + it('never maps a workspace package — those come from their own exports, or deliberately not at all', () => { + const root = tempTree({ + 'packages/pkg-a/package.json': JSON.stringify({ name: 'pkg-a', dependencies: { 'pkg-b': 'workspace:*' } }), + 'packages/pkg-a/node_modules/pkg-b/package.json': JSON.stringify({ name: 'pkg-b', types: 'src/index.ts' }), + 'packages/pkg-a/node_modules/pkg-b/src/index.ts': 'export const b = 1;\n', + }); + const { paths } = deriveDeclaredDependencyPaths(root, ['pkg-a'], { + 'pkg-a': 'packages/pkg-a', + 'pkg-b': 'packages/pkg-b', + }) as unknown as { paths: Record }; + expect(Object.keys(paths)).not.toContain('pkg-b'); + }); + + describe('in this repository', () => { + it("maps lucide-react, which the documented packages declare (objectui#6120)", () => { + const state = analyze({}) as unknown as { + dependencyPaths: Record; + dependencyDeclaredBy: Record; + }; + expect(Object.keys(state.dependencyPaths)).toContain('lucide-react'); + expect(state.dependencyPaths['lucide-react'][0]).toMatch(/\.d\.ts$/); + }); + + it('maps only declaration files, and never a package src/', () => { + const state = analyze({}) as unknown as { dependencyPaths: Record }; + const targets = Object.values(state.dependencyPaths).map((v) => v[0]); + expect(targets.length).toBeGreaterThan(10); + for (const target of targets) { + expect(target).toMatch(/\.d\.(ts|mts|cts)$/); + expect(target, 'a snippet must never be judged against a package src/').not.toMatch( + /[\\/]packages[\\/][^\\/]+[\\/]src[\\/]/, + ); + } + }); + + it('the UNDECLARED control specifier is installed here — otherwise it proves nothing', () => { + expect( + findInstalledCopy(repoRoot, UNDECLARED_CONTROL_PACKAGE), + `${UNDECLARED_CONTROL_PACKAGE} is not installed, so "it does not resolve" measures nothing`, + ).toBeTruthy(); + }); + + it('the UNDECLARED control specifier is declared by no workspace package at all', () => { + const packagesDir = path.join(repoRoot, 'packages'); + const declaring = fs + .readdirSync(packagesDir) + .filter((d) => fs.existsSync(path.join(packagesDir, d, 'package.json'))) + .filter((d) => { + const manifest = JSON.parse( + fs.readFileSync(path.join(packagesDir, d, 'package.json'), 'utf8'), + ) as { dependencies?: Record }; + return Boolean(manifest.dependencies?.[UNDECLARED_CONTROL_PACKAGE]); + }); + expect(declaring, 'pick a control specifier no package declares').toEqual([]); + }); + }); +}); + describe('wiring — a script nothing runs is not a gate', () => { const workflowDir = path.join(repoRoot, '.github/workflows'); const workflowPath = path.join(workflowDir, 'doc-snippet-types.yml'); diff --git a/scripts/check-doc-snippet-types.mjs b/scripts/check-doc-snippet-types.mjs index a5312c69a0..e574f02d1f 100644 --- a/scripts/check-doc-snippet-types.mjs +++ b/scripts/check-doc-snippet-types.mjs @@ -155,6 +155,68 @@ * types, unbuilt tree) turns every document red at once and reads * as "the docs are full of defects". * + * UNDECLARED a synthetic module importing `@floating-ui/react-dom` MUST produce + * TS2307. That package IS installed in this workspace — Radix's + * popper pulls it in, under `@object-ui/components`'s declared + * `@radix-ui/react-popover` — and NO package a covered document + * imports declares it, so no reader of the documented packages can + * import it either. It is the control on the third-party rule + * below: the moment resolution widens past what the imported + * packages declare, this control goes green and the gate has become + * a rubber stamp no snippet can fail — invisibly, because every + * document stays green while it happens. It also fails loudly if + * the specifier ever BECOMES a declared dependency (pick another), + * or is not installed at all (a specifier that resolves nowhere + * proves nothing about how far resolution reaches). + * + * ## Third-party specifiers resolve exactly as far as the imported packages declare + * + * A snippet that imports `@object-ui/layout` may also import `lucide-react`, + * because `@object-ui/layout` DECLARES `lucide-react`: a reader who installs that + * package gets it in their `node_modules`, and `SidebarNav`'s `NavItem.icon` + * genuinely takes a lucide icon. This program compiles every block at the + * repository ROOT, where under pnpm a workspace package's own dependency is not + * hoisted and so does not resolve — five correct blocks across + * `content/docs/layout` failed TS2307 on nothing but that (objectui#6120). The + * snippets were right; the resolution environment was the gap. + * + * The rule, stated here because its EDGES are the whole of its value: + * + * For every workspace package a COVERED document imports, each specifier that + * package declares in its own `dependencies` is mapped to the types a + * consumer of that package would resolve — resolved from inside that + * package's own directory, exactly the way that package's own code resolves + * it. + * + * Four edges, each deliberate: + * + * - **Declared, never merely installed.** The set comes from `dependencies` in + * the imported packages' manifests, never from a walk of `node_modules`. A + * blanket mapping would let a snippet import a transitive package no consumer + * can reach and still pass green, which is strictly worse than the gap it + * would close: the gate's whole value is that it fails where a reader fails. + * - **`dependencies` only** — not `peerDependencies`, not `devDependencies`. A + * dependency is what the package installs FOR its consumer; a peer is a + * requirement ON the consumer that may be unmet; a devDependency reaches no + * consumer at all. A snippet importing a peer therefore still fails here. + * That is the conservative direction on purpose: this rule fails CLOSED, and + * widening it later is a visible edit with a reason, not a silent drift. + * - **Imported packages only.** A package no covered document imports + * contributes nothing, so this map grows only as coverage grows — the same + * property `--build-filter` has, for the same reason. + * - **The bare specifier only, no subpath wildcard.** `lucide-react` is mapped; + * `lucide-react/dynamic` is not, and fails closed. Mapping `/*` would + * reach past the package's own `exports`, and `exports` is precisely the + * boundary a reader hits. + * + * ⛔ What this rule exists INSTEAD of: declaring `lucide-react` at the repository + * root. That would put an entry in this repository's dependency graph that exists + * only to make a checker pass — changing what the repo claims to need in order to + * satisfy a tool. The 2026-08-24 ruling on objectui#6120 rejected that route by + * name, alongside objectui#5329 (minting a `$schema` URL because prose named one) + * and objectui#6107 (minting exports because docs imported them). A manifest is a + * claim about what a package needs; a doc gate's convenience is not that claim. + * * ## Coverage is declared, never assumed — and the scan surface is stated here * * A document is covered unless it is named in `UNGATED_DOCS` with a reason. The @@ -597,6 +659,99 @@ export function derivePackageTypePaths(root = repoRoot) { return { paths, packageDirOf, sourceTyped }; } +/** A declaration file, in any of the three spellings a package may ship. */ +const DECLARATION_FILE = /\.d\.(ts|mts|cts)$/; +/** Any path inside a workspace package's `src/` — never a surface a reader gets. */ +const WORKSPACE_SRC = /[\\/]packages[\\/][^\\/]+[\\/]src[\\/]/; +/** Never written to disk: only a location to resolve FROM, inside a package. */ +const DEPENDENCY_PROBE_FILE = '__doc-snippet-dependency-probe__.ts'; + +/** + * `paths` for the THIRD-PARTY specifiers a covered snippet may legitimately + * import: for each workspace package a covered document imports, every specifier + * that package DECLARES in its own `dependencies`, resolved from inside that + * package's directory — which is exactly what the package's own code resolves, + * and exactly what a consumer who installs it gets. + * + * The rule and its four edges are stated in this file's header; the two things + * enforced right here are that the set is read from MANIFESTS (never from a walk + * of `node_modules`) and that a mapping may only ever land on a declaration file + * outside any package's `src/`. A specifier that ships no types is left + * unresolvable and reported as such, never mapped to something approximate: the + * snippet importing it then fails, which is the honest answer. + */ +export function deriveDeclaredDependencyPaths(root = repoRoot, importedPackages = [], packageDirOf = {}) { + const paths = {}; + const declaredBy = {}; + const untyped = []; + const seen = new Set(); + const options = { + module: COMPILER_OPTIONS.module, + moduleResolution: COMPILER_OPTIONS.moduleResolution, + }; + const host = ts.createCompilerHost(options, false); + // Sorted, so which package wins a specifier two of them declare is decided by + // name rather than by walk order — a run must not depend on readdir. + for (const owner of [...importedPackages].sort()) { + const dir = packageDirOf[owner]; + if (!dir) continue; + const manifestPath = join(root, dir, 'package.json'); + if (!existsSync(manifestPath)) continue; + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + for (const specifier of Object.keys(manifest.dependencies || {}).sort()) { + // A workspace package is mapped from its OWN `exports` by + // `derivePackageTypePaths`, and one deliberately left unmapped there + // (source-typed) must STAY unmapped — routing it through a node_modules + // symlink would judge a snippet against a package's `src/`, the exact + // substitution this gate exists to make impossible. + if (specifier in packageDirOf) continue; + if (seen.has(specifier)) continue; + seen.add(specifier); + const resolved = ts.resolveModuleName( + specifier, + join(root, dir, DEPENDENCY_PROBE_FILE), + options, + host, + ); + const file = resolved.resolvedModule ? resolved.resolvedModule.resolvedFileName : null; + if (!file || !DECLARATION_FILE.test(file) || WORKSPACE_SRC.test(file)) { + untyped.push({ specifier, owner, resolved: file }); + continue; + } + paths[specifier] = [file]; + declaredBy[specifier] = owner; + } + } + // `seen` is exactly the set of non-workspace specifiers the imported packages + // DECLARE, whether or not each one could be mapped. The UNDECLARED control + // reads it to tell its two failure modes apart: a control specifier that has + // become a declared dependency (pick another) is a different fact from one + // that resolves without any manifest declaring it (resolution has widened). + return { paths, declaredBy, untyped, declared: [...seen].sort() }; +} + +/** + * Where a package is physically installed in this workspace, found WITHOUT + * assuming it resolves from anywhere in particular: pnpm's virtual store holds + * one directory per (package, version, peer-set), named with `/` replaced by `+`. + * Used only by the UNDECLARED control, which must never confuse "this specifier + * does not resolve" with "this package is not installed" — the second proves + * nothing about how far resolution reaches. + */ +export function findInstalledCopy(root = repoRoot, specifier = '') { + const storeDir = join(root, 'node_modules', '.pnpm'); + if (!existsSync(storeDir)) return null; + const prefix = `${specifier.replace(/\//g, '+')}@`; + for (const entry of readdirSync(storeDir).sort()) { + if (!entry.startsWith(prefix)) continue; + const candidate = join(storeDir, entry, 'node_modules', ...specifier.split('/')); + if (existsSync(join(candidate, 'package.json'))) { + return relative(root, candidate).split(sep).join('/'); + } + } + return null; +} + /** Workspace package specifiers a document imports (bare specifier root only). */ function importedSpecifiers(body) { const out = new Set(); @@ -618,6 +773,18 @@ const SENTINEL_EXPORT = 'ThisNameIsDefinitelyNotExported'; const CONTROL_PACKAGE = '@object-ui/types'; const CONTROL_REAL_EXPORT = 'ComponentSchema'; +/** + * The UNDECLARED control's specifier (see the header). Three properties make it + * the right one, and all three are ASSERTED at run time rather than trusted: + * it is installed in this workspace (a transitive of `@radix-ui/react-popover`, + * which `@object-ui/components` declares), it is declared by no package in this + * repository at all, and it ships real `.d.ts` files — so if resolution ever did + * widen to reach it, the control module would compile CLEANLY rather than fail + * for some unrelated reason. It is the difference between "the rule is narrow" + * and "we hope the rule is narrow". + */ +const UNDECLARED_CONTROL_PACKAGE = '@floating-ui/react-dom'; + const COMPILER_OPTIONS = { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.ESNext, @@ -723,11 +890,36 @@ export function analyze({ root = repoRoot, ungated = UNGATED_DOCS } = {}) { } } - return { documents, covered, compiled, declaredFragments, findings, paths, neededPackages, scans }; + // ── and what THOSE packages declare resolves too, exactly that far ──────── + const { + paths: dependencyPaths, + declaredBy: dependencyDeclaredBy, + untyped: untypedDependencies, + declared: declaredSpecifiers, + } = deriveDeclaredDependencyPaths(root, neededPackages, packageDirOf); + // Workspace entries win every collision: a workspace package is mapped from + // its own `exports`, and one deliberately left unmapped stays unmapped. + const mergedPaths = { ...dependencyPaths, ...paths }; + + return { + documents, + covered, + compiled, + declaredFragments, + findings, + paths: mergedPaths, + workspacePaths: paths, + dependencyPaths, + dependencyDeclaredBy, + untypedDependencies, + declaredSpecifiers, + neededPackages, + scans, + }; } /** Phase 1 (syntax) and phase 2 (semantics), kept apart on purpose. */ -export function compileSnippets({ root = repoRoot, compiled, paths }) { +export function compileSnippets({ root = repoRoot, compiled, paths, declaredSpecifiers = [] }) { const parseFailures = []; const virtual = new Map(); const owners = new Map(); @@ -763,6 +955,14 @@ export function compileSnippets({ root = repoRoot, compiled, paths }) { positiveFile, `import type { ${CONTROL_REAL_EXPORT} } from '${CONTROL_PACKAGE}';\nexport type Control = ${CONTROL_REAL_EXPORT};\n`, ); + // A namespace import, so that ANY successful resolution reports zero + // diagnostics: the control must distinguish "did not resolve" from "resolved", + // never "resolved but the name I picked happened to be missing". + const undeclaredFile = join(root, VIRTUAL_DIR, '__control_undeclared.ts'); + virtual.set( + undeclaredFile, + `import * as undeclared from '${UNDECLARED_CONTROL_PACKAGE}';\nexport type Undeclared = typeof undeclared;\n`, + ); const options = { ...COMPILER_OPTIONS, baseUrl: root, paths, types: [] }; const host = ts.createCompilerHost(options, true); @@ -795,6 +995,7 @@ export function compileSnippets({ root = repoRoot, compiled, paths }) { const sentinelDiagnostics = [...program.getSemanticDiagnostics(program.getSourceFile(sentinelFile))]; const positiveDiagnostics = [...program.getSemanticDiagnostics(program.getSourceFile(positiveFile))]; + const undeclaredDiagnostics = [...program.getSemanticDiagnostics(program.getSourceFile(undeclaredFile))]; return { parseFailures, @@ -804,6 +1005,9 @@ export function compileSnippets({ root = repoRoot, compiled, paths }) { srcLeaks, sentinelDiagnostics, positiveDiagnostics, + undeclaredDiagnostics, + undeclaredDeclared: declaredSpecifiers.includes(UNDECLARED_CONTROL_PACKAGE), + undeclaredInstalledAt: findInstalledCopy(root, UNDECLARED_CONTROL_PACKAGE), }; } @@ -845,10 +1049,20 @@ function main() { return 1; } - const run = compileSnippets({ root: repoRoot, compiled: state.compiled, paths: state.paths }); + const run = compileSnippets({ + root: repoRoot, + compiled: state.compiled, + paths: state.paths, + declaredSpecifiers: state.declaredSpecifiers, + }); // ── controls, before any verdict about the documents ────────────────────── const controlFailures = []; + // Printed BEFORE the controls: it says how far this run's resolution reaches, + // which is the thing the UNDECLARED control then bounds. + console.log( + `Third-party resolution: ${Object.keys(state.dependencyPaths).length} specifier(s) mapped from the declared dependencies of ${state.neededPackages.size} imported package(s); ${state.untypedDependencies.length} declared specifier(s) ship no types here and stay unresolvable.`, + ); console.log('Controls:'); console.log( ` resolution Module name '${CONTROL_PACKAGE}' was successfully resolved to '${run.resolvedFileName ?? '(unresolved)'}'`, @@ -876,6 +1090,23 @@ function main() { `the positive control failed (${ts.flattenDiagnosticMessageText(run.positiveDiagnostics[0].messageText, ' ')}) — the harness is broken, not the documents`, ); } + const undeclaredCodes = run.undeclaredDiagnostics.map((d) => d.code); + console.log( + ` undeclared importing '${UNDECLARED_CONTROL_PACKAGE}' (installed at ${run.undeclaredInstalledAt ?? '(NOT INSTALLED)'}, declared by no imported package) produced ${run.undeclaredDiagnostics.length} diagnostic(s)${undeclaredCodes.length ? ` (TS${undeclaredCodes.join(', TS')})` : ''}`, + ); + if (run.undeclaredDeclared) { + controlFailures.push( + `'${UNDECLARED_CONTROL_PACKAGE}' is now a DECLARED dependency of a package a covered document imports, so it can no longer show that resolution stayed narrow — pick a control specifier no imported package declares`, + ); + } else if (!run.undeclaredInstalledAt) { + controlFailures.push( + `'${UNDECLARED_CONTROL_PACKAGE}' is not installed in this workspace, so its failure to resolve proves nothing about how far resolution reaches — pick an installed specifier no imported package declares`, + ); + } else if (!undeclaredCodes.includes(2307)) { + controlFailures.push( + `a specifier NO imported package declares now resolves — third-party resolution has widened past the imported packages' own dependencies, so a snippet may import what no reader of these packages can get, and every document would stay green while it does`, + ); + } console.log(''); const total = state.compiled.length + state.declaredFragments.length; @@ -935,4 +1166,4 @@ if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import process.exit(main()); } -export { UNGATED_DOCS, TS_FENCE_LANGUAGES, FRAGMENT_MARKER, main }; +export { UNGATED_DOCS, TS_FENCE_LANGUAGES, FRAGMENT_MARKER, UNDECLARED_CONTROL_PACKAGE, main };