From 1f5b27e9edc18d7b5a197345f0b43da9a9b21d6b Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Tue, 29 Sep 2026 23:19:45 +0100 Subject: [PATCH] test: guard the declaration shape that keeps tree schemas re-exportable A consumer re-exporting z.array(PredicateNodeSchema) without an annotation hit TS7056 when the declaration bundler emitted bare declarations plus a trailing export list. Assert every top-level declaration in the built tree declarations is exported in place. --- .../trilean/test/declaration-shape.test.ts | 34 +++++++++++++++++++ packages/trilean/vitest.config.ts | 4 +-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 packages/trilean/test/declaration-shape.test.ts diff --git a/packages/trilean/test/declaration-shape.test.ts b/packages/trilean/test/declaration-shape.test.ts new file mode 100644 index 0000000..cf0294f --- /dev/null +++ b/packages/trilean/test/declaration-shape.test.ts @@ -0,0 +1,34 @@ +// Guards the shape of the built declaration files for the two recursive node trees: every top-level declaration in dist/tree.d.ts and dist/tree.d.cts carries its own `export` modifier, none is declared bare and re-exported through a trailing `export { ... }` list. +// +// Why the shape matters: a consumer that re-exports `z.array(PredicateNodeSchema)` (or the expression equivalent) without an annotation makes TypeScript serialise the inferred type into the consumer's own declaration output. When the tree's declarations are exported in place, TypeScript can name them and the output stays small. When the declaration bundler emits bare `declare const X` plus a trailing `export { X }` list (tsdown before 0.23 did), the consumer's inferred type expands the whole recursive tree and fails with TS7056 ("the inferred type of this node exceeds the maximum length the compiler will serialize") in tsdown, tsc and any other declaration generator. The shape is decided by the bundler, not by anything in src/tree.ts, so a bundler change can reintroduce the failure with no diff to the schemas; this test is what would notice. +// +// Checks the built package like smoke.test.ts does (the `_test:smoke` turbo task depends on `_build`), and is a member of tsconfig.node.json's program, so it may use Node APIs. + +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +/** Matches a top-level declaration line that lacks `export`: `declare const ...`, `type X = ...`, `interface X ...`. Indented lines belong to a declaration body and are ignored by the anchor. */ +const bareTopLevelDeclaration = /^(?:declare|type|interface|const)\s/m; + +/** Matches the trailing `export { ... }` list a bundler emits when declarations are not exported in place. */ +const trailingExportList = /^export \{/m; + +describe.each(["dist/tree.d.ts", "dist/tree.d.cts"])( + "declaration shape of %s", + (relativePath) => { + const source = readFileSync( + new URL(`../${relativePath}`, import.meta.url), + "utf8", + ); + + it("exports both node-tree schemas in place", () => { + expect(source).toMatch(/^export declare const PredicateNodeSchema:/m); + expect(source).toMatch(/^export declare const ExpressionNodeSchema:/m); + }); + + it("declares nothing bare and re-exports nothing through a trailing list", () => { + expect(source).not.toMatch(bareTopLevelDeclaration); + expect(source).not.toMatch(trailingExportList); + }); + }, +); diff --git a/packages/trilean/vitest.config.ts b/packages/trilean/vitest.config.ts index 579de16..01694f4 100644 --- a/packages/trilean/vitest.config.ts +++ b/packages/trilean/vitest.config.ts @@ -27,10 +27,10 @@ export default defineConfig({ }, }, { - // Checks the built package rather than src/: dist/'s ESM and CJS entry points resolved through package.json's own `exports` map, and the generated schemas/trilean.schema.json. The `_test:smoke` turbo task depends on `_build`, so the output under test is always rebuilt from current source rather than whatever dist/ happened to be left lying around. See test/smoke.test.ts. + // Checks the built package rather than src/: dist/'s ESM and CJS entry points resolved through package.json's own `exports` map, and the generated schemas/trilean.schema.json. The `_test:smoke` turbo task depends on `_build`, so the output under test is always rebuilt from current source rather than whatever dist/ happened to be left lying around. See test/smoke.test.ts and test/declaration-shape.test.ts. test: { name: "smoke", - include: ["test/smoke.test.ts"], + include: ["test/smoke.test.ts", "test/declaration-shape.test.ts"], }, }, {