diff --git a/.changeset/11573-any-component-emit-headroom.md b/.changeset/11573-any-component-emit-headroom.md new file mode 100644 index 0000000000..3c34851bbe --- /dev/null +++ b/.changeset/11573-any-component-emit-headroom.md @@ -0,0 +1,32 @@ +--- +'@object-ui/types': patch +--- + +fix(types): `AnyComponentSchema`'s declaration prints every category union by name, so `@object-ui/types` no longer sits at the edge of TypeScript's serialization ceiling (objectui#11573) + +`tsc` prints an inferred type in full wherever a declaration uses it, and past +its serialization ceiling it refuses with TS7056 ("The inferred type of this +node exceeds the maximum length the compiler will serialize"). `@object-ui/types` +then emits no declarations, and every consumer of `@object-ui/types/zod` fails +with TS7016. `AnyComponentSchema` listed every category union inline, so its +declaration printed the sum of all of them: measured with TypeScript's own +counter, it read under one percent short of the ceiling against +`@objectstack/spec` built from objectstack `main`, and the Spec Main Shape Gate +had already gone red on it once. + +Each of the sixteen category unions `AnyComponentSchema` lists now has a named +type in its own module — an interface that extends the union's inferred type +and adds no member (`LayoutZodType`, `PublicBlockComponentZodType` and their +siblings) — and `AnyComponentSchema`'s declaration prints a reference to each +instead of its body. + +What changes for a consumer is the printed `.d.ts` TEXT of existing exports, +and nothing else: `AnyComponentSchema` and the sixteen unions are the same +values, and every type read through `z.input` / `z.output` is unchanged. The +new names are exported from their own modules, which is what lets the +declaration reference them; they are not added to the `@object-ui/types/zod` +entry, so nothing new is importable. + +`packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts` reads +the declaration's size with the compiler's own counter, fails when it crosses a +tenth of the ceiling below it, and lists every member as named or inline. diff --git a/packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts b/packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts new file mode 100644 index 0000000000..e3a99bac77 --- /dev/null +++ b/packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts @@ -0,0 +1,375 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `AnyComponentSchema`'s declaration stays well under TypeScript's + * serialization ceiling, and every member it lists is named or inline on + * purpose (objectui#11573). + * + * ## The defect these pin + * + * `tsc` prints an inferred type in full wherever a declaration uses it, and + * past its serialization ceiling it refuses with TS7056 ("The inferred type of + * this node exceeds the maximum length the compiler will serialize"). + * `@object-ui/types` then emits no declarations, and every consumer of + * `@object-ui/types/zod` fails with TS7016. `AnyComponentSchema` lists every + * category union, so its declaration printed the SUM of their prints. The + * `Spec Main Shape Gate` (which compiles this repository against + * `@objectstack/spec` built from objectstack `main`) went red with exactly that + * on objectui#11570, and single arms had been named one TS7056 at a time before + * it. objectui#11573 named the type of every category union instead; the why + * is the "Why every category union's TYPE is named" section on + * `AnyComponentSchema` in `../zod/index.zod.ts`. + * + * ## The instrument: the compiler's own counter, not a proxy + * + * The ceiling is a comparison inside TypeScript's node builder: it adds an + * estimate to `approximateLength` for every node it prints and, under + * `NoTruncation` (which declaration emit sets), reports TS7056 once that + * exceeds `noTruncationMaximumTruncationLength`. Neither number is on the typed + * public surface. What is reachable at run time is the builder's own + * `maximumLength` parameter, which `checker.typeToTypeNode` passes straight + * through: called with declaration emit's flags, the declaration's type and a + * stated maximum, the builder reports truncation to the tracker exactly when the + * counter crosses that maximum, so a search over the maximum reads the counter. + * Both the parameter and the ceiling are internal, so the first assertion below + * is a lit control: a maximum the declaration cannot fit under MUST report + * truncation. If a TypeScript upgrade drops or moves the parameter, that control + * goes red rather than this file going quietly green. + * + * ⚠️ The builder caches what it serialized per enclosing node, truncation + * verdict included, so one search would read its own earlier answers. The + * tracker answers `trackSymbol` with `true`, which marks the call as having + * reported a diagnostic, and the builder caches nothing from such a call. + * + * ## The margin, and which spec it is read against + * + * The pin fails when the reading crosses {@link MARGIN} below the ceiling. + * It runs under the INSTALLED `@objectstack/spec` (the lockfile's), while the + * Spec Main Shape Gate compiles against objectstack `main`, which is ahead of + * it. Before the naming, the two readings of this declaration differed by about + * one percent of the ceiling, the size of a spec delta, and the gate was the + * only one to see it; since the naming, the spec's shapes are printed inside + * the named unions' own declarations, and the readings were equal on both specs + * when objectui#11573 took them. The margin is a tenth of the ceiling: many times + * that delta and many times the largest arm ever added inline, so a reading under + * it here leaves the gate's compile under the ceiling with room to spare. The + * readings themselves are in objectui#11573's pull request, not here: a figure + * written into this comment would be derived once and never again. + * + * ## What each assertion catches + * + * 1. The lit control: the counter is reachable, so the reading can fail. + * 2. The reading: `AnyComponentSchema`'s declaration under the margin. + * 3. The enumeration: every member, in source order, as named (and by which + * name it prints) or inline. A new member, a member that loses its name, + * or a renamed type turns it red until it is classified here. + * 4. The rule behind the enumeration: a member that is a category union + * (a `z.discriminatedUnion` of arms) prints by name. Single arms may stay + * inline; the reading in 2 bounds what they cost. + * 5. The compile-time rows below the suites: each named type IS its union, + * so naming moved the printed text and no type a consumer reads. + */ + +import { describe, expect, it } from 'vitest'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; +import type { z } from 'zod'; +import { AnyComponentSchema } from '../zod/index.zod.js'; +import type { AIComponentZodType } from '../zod/ai.zod.js'; +import type { ComplexZodType } from '../zod/complex.zod.js'; +import type { CRUDComponentZodType } from '../zod/crud.zod.js'; +import type { DataDisplayZodType } from '../zod/data-display.zod.js'; +import type { DesignerUnionZodType } from '../zod/designer.zod.js'; +import type { DisclosureZodType } from '../zod/disclosure.zod.js'; +import type { FeedbackZodType } from '../zod/feedback.zod.js'; +import type { FormComponentZodType } from '../zod/form.zod.js'; +import type { LayoutZodType } from '../zod/layout.zod.js'; +import type { NavigationZodType } from '../zod/navigation.zod.js'; +import type { ObjectQLComponentZodType, ObjectQLPublicBlockComponentZodType } from '../zod/objectql.zod.js'; +import type { OverlayZodType } from '../zod/overlay.zod.js'; +import type { PublicBlockComponentZodType } from '../zod/public-blocks.zod.js'; +import type { ReportUnionZodType } from '../zod/reports.zod.js'; +import type { ViewComponentZodType } from '../zod/views.zod.js'; + +const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const ENTRY = join(packageRoot, 'src', 'zod', 'index.zod.ts'); + +/** TypeScript's own ceiling, read from the compiler rather than restated here. */ +const CEILING = (ts as unknown as { noTruncationMaximumTruncationLength: number }).noTruncationMaximumTruncationLength; + +/** A tenth of the ceiling; the header says why it covers the gap to the gate's spec. */ +const MARGIN = CEILING / 10; + +/** + * Declaration emit's node-builder flags: `MultilineObjectLiterals`, + * `WriteClassExpressionAsTypeLiteral`, `UseTypeOfFunction`, + * `UseStructuralFallback`, `AllowEmptyTuple`, `GenerateNamesForShadowedTypeParams` + * and `NoTruncation`, which is the one that makes a crossing a TS7056. + */ +const DECLARATION_EMIT_FLAGS = + ts.NodeBuilderFlags.MultilineObjectLiterals + | ts.NodeBuilderFlags.WriteClassExpressionAsTypeLiteral + | ts.NodeBuilderFlags.UseTypeOfFunction + | ts.NodeBuilderFlags.UseStructuralFallback + | ts.NodeBuilderFlags.AllowEmptyTuple + | ts.NodeBuilderFlags.GenerateNamesForShadowedTypeParams + | ts.NodeBuilderFlags.NoTruncation; + +/** Declaration emit's internal flag (`AllowUnresolvedNames`); internal, so spelled as its value. */ +const DECLARATION_EMIT_INTERNAL_FLAGS = 8; + +/** The run-time signature `checker.typeToTypeNode` has beyond its typed three parameters. */ +type TypeToTypeNodeAtRunTime = ( + type: ts.Type, + enclosingDeclaration: ts.Node | undefined, + flags: ts.NodeBuilderFlags, + internalFlags: number, + tracker: { trackSymbol(): boolean; reportTruncationError(): void }, + maximumLength?: number, +) => ts.TypeNode | undefined; + +/** The package's own program, its build options, with the installed spec. */ +function loadProgram(): ts.Program { + const configPath = join(packageRoot, 'tsconfig.json'); + const readConfig = ts.readConfigFile(configPath, ts.sys.readFile); + if (readConfig.error) { + throw new Error(ts.flattenDiagnosticMessageText(readConfig.error.messageText, '\n')); + } + const parsed = ts.parseJsonConfigFileContent(readConfig.config, ts.sys, packageRoot); + return ts.createProgram([ENTRY], { + ...parsed.options, + noEmit: true, + composite: false, + incremental: false, + tsBuildInfoFile: undefined, + }); +} + +const program = loadProgram(); +const checker = program.getTypeChecker(); +const entryFile = program.getSourceFile(ENTRY); +if (!entryFile) throw new Error(`${ENTRY} is not in the program`); +const sourceFile: ts.SourceFile = entryFile; +const typeToTypeNode = checker.typeToTypeNode as unknown as TypeToTypeNodeAtRunTime; + +/** Does `type`, printed as declaration emit prints it, fit under `maximumLength`? */ +function fitsUnder(type: ts.Type, maximumLength: number): boolean { + let truncated = false; + typeToTypeNode(type, sourceFile, DECLARATION_EMIT_FLAGS, DECLARATION_EMIT_INTERNAL_FLAGS, { + // `true` marks the call as having reported a diagnostic, so the builder + // caches nothing from it (see the header). + trackSymbol: () => true, + reportTruncationError: () => { + truncated = true; + }, + }, maximumLength); + return !truncated; +} + +/** + * The counter's reading: the smallest maximum `type` fits under, to within + * `precision`, or `Infinity` past the ceiling. + */ +function readCounter(type: ts.Type, precision = 1): number { + if (!fitsUnder(type, CEILING)) return Number.POSITIVE_INFINITY; + let truncates = 0; + let fits = CEILING; + while (fits - truncates > precision) { + const mid = Math.floor((truncates + fits) / 2); + if (fitsUnder(type, mid)) fits = mid; + else truncates = mid; + } + return fits; +} + +/** The name a type prints by in `index.zod.ts`, or `undefined` when it prints inline. */ +function printedName(type: ts.Type): string | undefined { + const node = typeToTypeNode(type, sourceFile, DECLARATION_EMIT_FLAGS, DECLARATION_EMIT_INTERNAL_FLAGS, { + trackSymbol: () => true, + reportTruncationError: () => {}, + }); + if (node && ts.isTypeReferenceNode(node) && !node.typeArguments) { + return ts.isIdentifier(node.typeName) ? node.typeName.text : node.typeName.right.text; + } + if (node && ts.isImportTypeNode(node) && !node.typeArguments && node.qualifier) { + return ts.isIdentifier(node.qualifier) ? node.qualifier.text : node.qualifier.right.text; + } + return undefined; +} + +/** `AnyComponentSchema`'s declaration, and the members its `z.discriminatedUnion` lists, from the AST. */ +function readDeclaration(): { declaration: ts.VariableDeclaration; members: ts.Identifier[] } { + let declaration: ts.VariableDeclaration | undefined; + for (const statement of sourceFile.statements) { + if (!ts.isVariableStatement(statement)) continue; + for (const d of statement.declarationList.declarations) { + if (ts.isIdentifier(d.name) && d.name.text === 'AnyComponentSchema') declaration = d; + } + } + if (!declaration?.initializer) throw new Error('no `AnyComponentSchema` declaration in index.zod.ts'); + let list: ts.ArrayLiteralExpression | undefined; + const find = (node: ts.Node): void => { + if (list) return; + if ( + ts.isCallExpression(node) + && node.expression.getText(sourceFile) === 'z.discriminatedUnion' + && node.arguments[1] + && ts.isArrayLiteralExpression(node.arguments[1]) + ) { + list = node.arguments[1]; + return; + } + ts.forEachChild(node, find); + }; + find(declaration.initializer); + if (!list) throw new Error('`AnyComponentSchema` lists no `z.discriminatedUnion` members'); + const members = list.elements.map((element) => { + if (!ts.isIdentifier(element)) throw new Error(`a member that is not a plain name: ${element.getText(sourceFile)}`); + return element; + }); + return { declaration, members }; +} + +const { declaration, members } = readDeclaration(); +const declarationType = checker.getTypeAtLocation(declaration.name); + +/** Is the member a category union, that is a `z.discriminatedUnion` of arms? */ +function isCategoryUnion(member: ts.Identifier): boolean { + return checker.getPropertiesOfType(checker.getTypeAtLocation(member)).some((p) => p.getName() === 'options'); +} + +/** Every member, in source order: its kind, and the name it prints by, or `inline`. */ +const table = members.map((member) => ({ + member: member.text, + kind: isCategoryUnion(member) ? 'category union' : 'arm', + printsAs: printedName(checker.getTypeAtLocation(member)) ?? 'inline', +})); + +/** Read once, here: what the assertions below and their messages report. */ +const reading = readCounter(declarationType); + +/** What the members printed inline cost, to the nearest thousand, for the failure message. */ +function inlineCosts(): string { + return table + .filter((row) => row.printsAs === 'inline') + .map((row) => { + const member = members.find((m) => m.text === row.member); + return `${row.member} (${row.kind}): ${member ? readCounter(checker.getTypeAtLocation(member), 1000) : '?'}`; + }) + .join('; '); +} + +describe('`AnyComponentSchema` declaration emit headroom (objectui#11573)', () => { + it('reads the compiler\'s own counter: a maximum the declaration cannot fit under reports truncation (lit control)', () => { + expect(CEILING).toBeGreaterThan(0); + expect(fitsUnder(declarationType, 100)).toBe(false); + expect(fitsUnder(declarationType, CEILING)).toBe(true); + // Non-vacuity: the reading is a real search result, strictly inside (100, ceiling]. + expect(reading).toBeGreaterThan(100); + expect(reading).toBeLessThanOrEqual(CEILING); + }); + + it('stays at least a tenth of the ceiling below it', () => { + expect( + reading, + `AnyComponentSchema's declaration reads ${reading} on the compiler's counter, past ${CEILING - MARGIN} ` + + `(the ceiling ${CEILING} less the margin ${MARGIN}). Members printed inline: ${inlineCosts()}. ` + + 'Name the type of whatever grew, as the category unions are named.', + ).toBeLessThanOrEqual(CEILING - MARGIN); + }); + + it('lists every member as named, by the name it prints, or inline', () => { + // The AST walk read the same members the union holds at run time. + const runtimeOptions = (AnyComponentSchema as unknown as { options: unknown[] }).options; + expect(members.length).toBe(runtimeOptions.length); + expect(table).toEqual([ + { member: 'AppComponentSchema', kind: 'arm', printsAs: 'inline' }, + { member: 'AppSchemaRendererNodeSchema', kind: 'arm', printsAs: 'AppSchemaRendererNodeSchemaType' }, + { member: 'LayoutSchema', kind: 'category union', printsAs: 'LayoutZodType' }, + { member: 'PageKindNodeSchema', kind: 'arm', printsAs: 'PageKindNodeSchemaType' }, + { member: 'FormComponentSchema', kind: 'category union', printsAs: 'FormComponentZodType' }, + { member: 'DataDisplaySchema', kind: 'category union', printsAs: 'DataDisplayZodType' }, + { member: 'FeedbackSchema', kind: 'category union', printsAs: 'FeedbackZodType' }, + { member: 'DisclosureSchema', kind: 'category union', printsAs: 'DisclosureZodType' }, + { member: 'OverlaySchema', kind: 'category union', printsAs: 'OverlayZodType' }, + { member: 'NavigationSchema', kind: 'category union', printsAs: 'NavigationZodType' }, + { member: 'ComplexSchema', kind: 'category union', printsAs: 'ComplexZodType' }, + { member: 'ObjectQLComponentSchema', kind: 'category union', printsAs: 'ObjectQLComponentZodType' }, + { + member: 'ObjectQLPublicBlockComponentSchema', + kind: 'category union', + printsAs: 'ObjectQLPublicBlockComponentZodType', + }, + { member: 'CRUDComponentSchema', kind: 'category union', printsAs: 'CRUDComponentZodType' }, + { member: 'ReportUnionSchema', kind: 'category union', printsAs: 'ReportUnionZodType' }, + { member: 'ViewComponentSchema', kind: 'category union', printsAs: 'ViewComponentZodType' }, + { member: 'AIComponentSchema', kind: 'category union', printsAs: 'AIComponentZodType' }, + { member: 'DesignerUnionSchema', kind: 'category union', printsAs: 'DesignerUnionZodType' }, + { member: 'PublicBlockComponentSchema', kind: 'category union', printsAs: 'PublicBlockComponentZodType' }, + { member: 'CloudPlanStatusSchema', kind: 'arm', printsAs: 'inline' }, + ]); + }); + + it('prints every category union by name', () => { + expect(table.filter((row) => row.kind === 'category union' && row.printsAs === 'inline')).toEqual([]); + // Non-vacuity: the kind test finds category unions at all, and an inline arm. + expect(table.filter((row) => row.kind === 'category union').length).toBeGreaterThan(0); + expect(table.some((row) => row.kind === 'arm' && row.printsAs === 'inline')).toBe(true); + }); +}); + +/* + * Each named type IS its union: an interface that extends the union's inferred + * type and adds no member, so what a consumer reads through `z.input` / + * `z.output`, and the union's internals, are those of the discriminated union + * over its own options. `tsc -p tsconfig.test.json` (the package's + * `type-check`) is what evaluates these rows. + */ +type Assert = T; +type Equal = (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; + +/** The discriminated union over `N`'s own options: what the named type must be. */ +type UnionOver = z.ZodDiscriminatedUnion; +type IsItsUnion = [ + Equal, z.input>>, + Equal, z.output>>, + Equal['_zod']>, +] extends [true, true, true] ? true : false; +/** The named type is the member `AnyComponentSchema` carries, so its derived types read it. */ +type IsCarried = Equal, N>; +/** + * Both, as one verdict. ⛔ Not `A & B`: `true & false` is `never`, and `never` + * satisfies `Assert`'s `extends true`, so an intersection would pass a failing row. + */ +type NamesItsUnion = + [IsItsUnion, IsCarried] extends [true, true] ? true : false; + +type _Layout = Assert>; +type _FormComponent = Assert>; +type _DataDisplay = Assert>; +type _Feedback = Assert>; +type _Disclosure = Assert>; +type _Overlay = Assert>; +type _Navigation = Assert>; +type _Complex = Assert>; +type _ObjectQLComponent = Assert>; +type _ObjectQLPublicBlockComponent = Assert>; +type _CRUDComponent = Assert>; +type _ReportUnion = Assert>; +type _ViewComponent = Assert>; +type _AIComponent = Assert>; +type _DesignerUnion = Assert>; +type _PublicBlockComponent = Assert>; + +// Lit control: the rows distinguish two unions, so a named type that read another +// union's options, or added to what a consumer reads, could not pass them. +type _ControlAnotherUnionIsNotEqual = Assert< + Equal, z.input>> extends true ? false : true +>; diff --git a/packages/types/src/zod/ai.zod.ts b/packages/types/src/zod/ai.zod.ts index a40e67ff64..345db9ac0a 100644 --- a/packages/types/src/zod/ai.zod.ts +++ b/packages/types/src/zod/ai.zod.ts @@ -303,8 +303,21 @@ export const NLQuerySchema = BaseSchema.extend({ * Union of the AI component arms — the category member `AnyComponentSchema` * lists (objectui#10859). */ -export const AIComponentSchema = z.discriminatedUnion('type', [ +const AIComponentSchemaInferred = z.discriminatedUnion('type', [ AIFormAssistSchema, AIRecommendationsSchema, NLQuerySchema, ]); + +/** + * The TYPE of {@link AIComponentSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface AIComponentZodType extends AIComponentSchemaInferredType { + options: AIComponentSchemaInferredType['options']; +} +type AIComponentSchemaInferredType = typeof AIComponentSchemaInferred; + +/** The union above, typed by its named {@link AIComponentZodType}. */ +export const AIComponentSchema: AIComponentZodType = AIComponentSchemaInferred; diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index 73f5fd3b38..38d1b51c84 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -1643,7 +1643,7 @@ export const DashboardConfigSchema = z.object({ /** * Complex Schema Union - All complex component schemas */ -export const ComplexSchema = z.discriminatedUnion('type', [ +const ComplexSchemaInferred = z.discriminatedUnion('type', [ RetiredKanbanNodeSchema, CalendarViewSchema, FilterBuilderSchema, @@ -1653,3 +1653,16 @@ export const ComplexSchema = z.discriminatedUnion('type', [ ChatbotFloatingSchema, DashboardComponentSchema, ]); + +/** + * The TYPE of {@link ComplexSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface ComplexZodType extends ComplexSchemaInferredType { + options: ComplexSchemaInferredType['options']; +} +type ComplexSchemaInferredType = typeof ComplexSchemaInferred; + +/** The union above, typed by its named {@link ComplexZodType}. */ +export const ComplexSchema: ComplexZodType = ComplexSchemaInferred; diff --git a/packages/types/src/zod/crud.zod.ts b/packages/types/src/zod/crud.zod.ts index 05d5fb9899..587eff459f 100644 --- a/packages/types/src/zod/crud.zod.ts +++ b/packages/types/src/zod/crud.zod.ts @@ -282,7 +282,7 @@ export const CRUDDialogSchema = BaseSchema.extend({ * does. ⛔ Not an accept-set change: all three arms already declared `type` as a * distinct `z.literal` (`action` · `detail` · `crud-dialog`). */ -export const CRUDComponentSchema = z.discriminatedUnion('type', [ +const CRUDComponentSchemaInferred = z.discriminatedUnion('type', [ // `ActionSchema`'s `z.ZodType` annotation // is a maintainer ruling (objectui#7760, decision batch #69) and ⛔ does not // move here. That type declares `_zod.propValues` as `PropValues | undefined`, @@ -297,6 +297,19 @@ export const CRUDComponentSchema = z.discriminatedUnion('type', [ CRUDDialogSchema, ]); +/** + * The TYPE of {@link CRUDComponentSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface CRUDComponentZodType extends CRUDComponentSchemaInferredType { + options: CRUDComponentSchemaInferredType['options']; +} +type CRUDComponentSchemaInferredType = typeof CRUDComponentSchemaInferred; + +/** The union above, typed by its named {@link CRUDComponentZodType}. */ +export const CRUDComponentSchema: CRUDComponentZodType = CRUDComponentSchemaInferred; + /** * Export type inference helpers */ diff --git a/packages/types/src/zod/data-display.zod.ts b/packages/types/src/zod/data-display.zod.ts index 992ef53cb5..20c04d3b2d 100644 --- a/packages/types/src/zod/data-display.zod.ts +++ b/packages/types/src/zod/data-display.zod.ts @@ -1673,7 +1673,7 @@ export const BarChartSchema = BaseSchema.extend({ children: retirementTombstone(BAR_CHART_NEITHER_CHANNEL), }); -export const DataDisplaySchema = z.discriminatedUnion('type', [ +const DataDisplaySchemaInferred = z.discriminatedUnion('type', [ AlertSchema, StatisticSchema, BadgeSchema, @@ -1690,3 +1690,16 @@ export const DataDisplaySchema = z.discriminatedUnion('type', [ HtmlSchema, BarChartSchema, ]); + +/** + * The TYPE of {@link DataDisplaySchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface DataDisplayZodType extends DataDisplaySchemaInferredType { + options: DataDisplaySchemaInferredType['options']; +} +type DataDisplaySchemaInferredType = typeof DataDisplaySchemaInferred; + +/** The union above, typed by its named {@link DataDisplayZodType}. */ +export const DataDisplaySchema: DataDisplayZodType = DataDisplaySchemaInferred; diff --git a/packages/types/src/zod/designer.zod.ts b/packages/types/src/zod/designer.zod.ts index 6504cfc7bc..008342ec02 100644 --- a/packages/types/src/zod/designer.zod.ts +++ b/packages/types/src/zod/designer.zod.ts @@ -664,7 +664,7 @@ export const FieldDesignerSchema = BaseSchema.extend({ * because `DesignerComponentSchema` is the canvas component's mirror above * (the `ReportUnionSchema` precedent in `reports.zod.ts`). */ -export const DesignerUnionSchema = z.discriminatedUnion('type', [ +const DesignerUnionSchemaInferred = z.discriminatedUnion('type', [ PageDesignerSchema, DataModelDesignerSchema, ProcessDesignerSchema, @@ -672,3 +672,16 @@ export const DesignerUnionSchema = z.discriminatedUnion('type', [ ObjectManagerSchema, FieldDesignerSchema, ]); + +/** + * The TYPE of {@link DesignerUnionSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface DesignerUnionZodType extends DesignerUnionSchemaInferredType { + options: DesignerUnionSchemaInferredType['options']; +} +type DesignerUnionSchemaInferredType = typeof DesignerUnionSchemaInferred; + +/** The union above, typed by its named {@link DesignerUnionZodType}. */ +export const DesignerUnionSchema: DesignerUnionZodType = DesignerUnionSchemaInferred; diff --git a/packages/types/src/zod/disclosure.zod.ts b/packages/types/src/zod/disclosure.zod.ts index f7d88b6c50..0833fd7240 100644 --- a/packages/types/src/zod/disclosure.zod.ts +++ b/packages/types/src/zod/disclosure.zod.ts @@ -138,8 +138,21 @@ export const ToggleGroupSchema = BaseSchema.extend({ /** * Disclosure Schema Union - All disclosure component schemas */ -export const DisclosureSchema = z.discriminatedUnion('type', [ +const DisclosureSchemaInferred = z.discriminatedUnion('type', [ AccordionSchema, CollapsibleSchema, ToggleGroupSchema, ]); + +/** + * The TYPE of {@link DisclosureSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface DisclosureZodType extends DisclosureSchemaInferredType { + options: DisclosureSchemaInferredType['options']; +} +type DisclosureSchemaInferredType = typeof DisclosureSchemaInferred; + +/** The union above, typed by its named {@link DisclosureZodType}. */ +export const DisclosureSchema: DisclosureZodType = DisclosureSchemaInferred; diff --git a/packages/types/src/zod/feedback.zod.ts b/packages/types/src/zod/feedback.zod.ts index e237e49dd7..1174fc5e71 100644 --- a/packages/types/src/zod/feedback.zod.ts +++ b/packages/types/src/zod/feedback.zod.ts @@ -301,7 +301,7 @@ export const SonnerSchema = BaseSchema.extend({ /** * Feedback Schema Union - All feedback component schemas */ -export const FeedbackSchema = z.discriminatedUnion('type', [ +const FeedbackSchemaInferred = z.discriminatedUnion('type', [ LoadingSchema, ProgressSchema, SkeletonSchema, @@ -311,3 +311,16 @@ export const FeedbackSchema = z.discriminatedUnion('type', [ EmptySchema, SonnerSchema, ]); + +/** + * The TYPE of {@link FeedbackSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface FeedbackZodType extends FeedbackSchemaInferredType { + options: FeedbackSchemaInferredType['options']; +} +type FeedbackSchemaInferredType = typeof FeedbackSchemaInferred; + +/** The union above, typed by its named {@link FeedbackZodType}. */ +export const FeedbackSchema: FeedbackZodType = FeedbackSchemaInferred; diff --git a/packages/types/src/zod/form.zod.ts b/packages/types/src/zod/form.zod.ts index 3e1b429d79..fb770a8054 100644 --- a/packages/types/src/zod/form.zod.ts +++ b/packages/types/src/zod/form.zod.ts @@ -1389,7 +1389,7 @@ export const CodeEditorSchema = BaseSchema.extend({ children: retirementTombstone(CODE_EDITOR_NEITHER_CHANNEL), }); -export const FormComponentSchema = z.discriminatedUnion('type', [ +const FormComponentSchemaInferred = z.discriminatedUnion('type', [ ButtonSchema, InputSchema, TextareaSchema, @@ -1411,3 +1411,16 @@ export const FormComponentSchema = z.discriminatedUnion('type', [ InputShorthandSchema, UiCalendarSchema, ]); + +/** + * The TYPE of {@link FormComponentSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface FormComponentZodType extends FormComponentSchemaInferredType { + options: FormComponentSchemaInferredType['options']; +} +type FormComponentSchemaInferredType = typeof FormComponentSchemaInferred; + +/** The union above, typed by its named {@link FormComponentZodType}. */ +export const FormComponentSchema: FormComponentZodType = FormComponentSchemaInferred; diff --git a/packages/types/src/zod/index.zod.ts b/packages/types/src/zod/index.zod.ts index 6d1ea4d0dc..5f97f19888 100644 --- a/packages/types/src/zod/index.zod.ts +++ b/packages/types/src/zod/index.zod.ts @@ -589,6 +589,33 @@ import { nestedComponentJudgment } from './nested-component-walk.js'; * fill is an installation and nothing more. The slot itself is * still a plain `z.union` in `base.zod.ts` — that is what keeps its option array by * reference, and it is untouched by the discrimination. + * + * ## Why every category union's TYPE is named (objectui#11573) + * + * `tsc` prints an inferred type in full wherever a declaration uses it, and it + * refuses to print one past its serialization ceiling with TS7056 ("The + * inferred type of this node exceeds the maximum length the compiler will + * serialize"); `@object-ui/types` then emits no declarations at all, and every + * consumer of `./zod` fails with TS7016. This declaration lists the members + * below, so printed inline it is the SUM of every category's print — the + * `Spec Main Shape Gate` caught it crossing the ceiling, and single arms were + * named one TS7056 at a time to stay under it (`AppSchemaRendererNodeSchemaType`, + * `PageKindNodeSchemaType`, `ReportNodeZodType`, `RecordLineItemsBlockSchemaType`) + * until objectui#11573 measured what was left: under one percent of the ceiling. + * + * So every category union listed here has a NAMED type: an exported interface + * in its own module that extends the union's inferred type and adds no member + * (`LayoutZodType`, `PublicBlockComponentZodType` and their siblings). An + * interface is printed by name, so each union's body is printed once, in its + * own module's declaration, and this declaration prints a reference to it. The + * TYPES are unchanged: the value is the union itself, and `z.input` / `z.output` + * read the union's own internals. Only the printed `.d.ts` text moved. + * + * ⚠️ A member added below is printed inline unless its type is named the same + * way. `__tests__/any-component-emit-headroom-11573.test.ts` reads this + * declaration's size with the compiler's own counter, fails when it crosses its + * stated margin below the ceiling, and lists every member as named or inline, + * so a new member turns it red until it is classified there. */ export const AnyComponentSchema = defineNodeComponentUnion(z.discriminatedUnion('type', [ AppComponentSchema, diff --git a/packages/types/src/zod/layout.zod.ts b/packages/types/src/zod/layout.zod.ts index b761015113..b126c32bcd 100644 --- a/packages/types/src/zod/layout.zod.ts +++ b/packages/types/src/zod/layout.zod.ts @@ -1518,7 +1518,7 @@ export const HtmlElementSchema = BaseSchema.extend({ /** * Layout Schema Union - All layout component schemas */ -export const LayoutSchema = z.discriminatedUnion('type', [ +const LayoutSchemaInferred = z.discriminatedUnion('type', [ DivSchema, BoxSchema, TextSpanSchema, @@ -1542,3 +1542,16 @@ export const LayoutSchema = z.discriminatedUnion('type', [ SemanticElementSchema, HtmlElementSchema, ]); + +/** + * The TYPE of {@link LayoutSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface LayoutZodType extends LayoutSchemaInferredType { + options: LayoutSchemaInferredType['options']; +} +type LayoutSchemaInferredType = typeof LayoutSchemaInferred; + +/** The union above, typed by its named {@link LayoutZodType}. */ +export const LayoutSchema: LayoutZodType = LayoutSchemaInferred; diff --git a/packages/types/src/zod/navigation.zod.ts b/packages/types/src/zod/navigation.zod.ts index b6785d79f4..33d5243d59 100644 --- a/packages/types/src/zod/navigation.zod.ts +++ b/packages/types/src/zod/navigation.zod.ts @@ -375,7 +375,7 @@ export const ButtonGroupSchema = BaseSchema.extend({ /** * Navigation Schema Union - All navigation component schemas */ -export const NavigationSchema = z.discriminatedUnion('type', [ +const NavigationSchemaInferred = z.discriminatedUnion('type', [ HeaderBarSchema, SidebarSchema, BreadcrumbSchema, @@ -383,3 +383,16 @@ export const NavigationSchema = z.discriminatedUnion('type', [ NavigationMenuSchema, ButtonGroupSchema, ]); + +/** + * The TYPE of {@link NavigationSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface NavigationZodType extends NavigationSchemaInferredType { + options: NavigationSchemaInferredType['options']; +} +type NavigationSchemaInferredType = typeof NavigationSchemaInferred; + +/** The union above, typed by its named {@link NavigationZodType}. */ +export const NavigationSchema: NavigationZodType = NavigationSchemaInferred; diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 4abab7e182..18f83bb995 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -4189,7 +4189,7 @@ export const ObjectDataTableSchema = BaseSchema.extend({ * `object-grid` node is armed by `ObjectGridBlockSchema` below, from its spec * row. */ -export const ObjectQLComponentSchema = z.discriminatedUnion('type', [ +const ObjectQLComponentSchemaInferred = z.discriminatedUnion('type', [ ObjectViewSchema, ObjectTreeSchema, ObjectCalendarSchema, @@ -4199,6 +4199,19 @@ export const ObjectQLComponentSchema = z.discriminatedUnion('type', [ ListViewSchema, ]); +/** + * The TYPE of {@link ObjectQLComponentSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface ObjectQLComponentZodType extends ObjectQLComponentSchemaInferredType { + options: ObjectQLComponentSchemaInferredType['options']; +} +type ObjectQLComponentSchemaInferredType = typeof ObjectQLComponentSchemaInferred; + +/** The union above, typed by its named {@link ObjectQLComponentZodType}. */ +export const ObjectQLComponentSchema: ObjectQLComponentZodType = ObjectQLComponentSchemaInferred; + /* ── ADR-0080 public blocks of this family, armed from their spec rows ───── */ // The `properties` member of each block below is `propsBag` from @@ -5154,7 +5167,7 @@ export const EmbeddableFormBlockSchema = BaseSchema.extend({ * declaration, not a copy of it. `ObjectGridSchema` and `ObjectChartSchema` * above spread it too (objectui#10872 batch 9). */ -export const ObjectQLPublicBlockComponentSchema = z.discriminatedUnion('type', [ +const ObjectQLPublicBlockComponentSchemaInferred = z.discriminatedUnion('type', [ ObjectMetricBlockSchema, ObjectMasterDetailFormBlockSchema, ObjectTimelineBlockSchema, @@ -5167,3 +5180,16 @@ export const ObjectQLPublicBlockComponentSchema = z.discriminatedUnion('type', [ ObjectPivotBlockSchema, EmbeddableFormBlockSchema, ]); + +/** + * The TYPE of {@link ObjectQLPublicBlockComponentSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface ObjectQLPublicBlockComponentZodType extends ObjectQLPublicBlockComponentSchemaInferredType { + options: ObjectQLPublicBlockComponentSchemaInferredType['options']; +} +type ObjectQLPublicBlockComponentSchemaInferredType = typeof ObjectQLPublicBlockComponentSchemaInferred; + +/** The union above, typed by its named {@link ObjectQLPublicBlockComponentZodType}. */ +export const ObjectQLPublicBlockComponentSchema: ObjectQLPublicBlockComponentZodType = ObjectQLPublicBlockComponentSchemaInferred; diff --git a/packages/types/src/zod/overlay.zod.ts b/packages/types/src/zod/overlay.zod.ts index 13f3abe0d4..7e2280f8a9 100644 --- a/packages/types/src/zod/overlay.zod.ts +++ b/packages/types/src/zod/overlay.zod.ts @@ -562,7 +562,7 @@ export const MenubarSchema = BaseSchema.extend({ /** * Overlay Schema Union - All overlay component schemas */ -export const OverlaySchema = z.discriminatedUnion('type', [ +const OverlaySchemaInferred = z.discriminatedUnion('type', [ DialogSchema, AlertDialogSchema, SheetSchema, @@ -574,3 +574,16 @@ export const OverlaySchema = z.discriminatedUnion('type', [ ContextMenuSchema, MenubarSchema, ]); + +/** + * The TYPE of {@link OverlaySchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface OverlayZodType extends OverlaySchemaInferredType { + options: OverlaySchemaInferredType['options']; +} +type OverlaySchemaInferredType = typeof OverlaySchemaInferred; + +/** The union above, typed by its named {@link OverlayZodType}. */ +export const OverlaySchema: OverlayZodType = OverlaySchemaInferred; diff --git a/packages/types/src/zod/public-blocks.zod.ts b/packages/types/src/zod/public-blocks.zod.ts index f42de7e8e8..64b8d1630f 100644 --- a/packages/types/src/zod/public-blocks.zod.ts +++ b/packages/types/src/zod/public-blocks.zod.ts @@ -1082,16 +1082,18 @@ type RecordLineItemsBlockArmType = typeof RecordLineItemsBlockArm; * ## Why (objectui#10872) * * `tsc` prints an inferred type in full wherever a declaration uses it. So - * `AnyComponentSchema`'s declaration in `index.zod.d.ts` spells out every - * arm of every union it lists. That declaration sits close to the compiler's - * serialization ceiling. Measured on objectstack main 5c9138b, this arm's own + * `AnyComponentSchema`'s declaration in `index.zod.d.ts` spelled out every + * arm of every union it listed, and sat close to the compiler's serialization + * ceiling. Measured on objectstack main 5c9138b, this arm's own * serialized size is the same as against 17.6.0; what grew is the rest of * `AnyComponentSchema`, which left too little room under the ceiling for the * arm inline. Printed inline, it made `@object-ui/types` fail with TS7056 at * `AnyComponentSchema` and emit no declarations; the Spec Main Shape Gate * caught it. An interface is printed by name, so naming the arm's type takes - * its body out of the union's print: it is printed once, here. The family and - * the remaining headroom are tracked in objectui#11573. + * its body out of the union's print: it is printed once, here. objectui#11573 + * closed the family by naming every category union `AnyComponentSchema` lists, + * this one included (`PublicBlockComponentZodType`), and + * `../__tests__/any-component-emit-headroom-11573.test.ts` reads the headroom. * * ## Why the TYPE is unchanged * @@ -1554,7 +1556,7 @@ export const ActionMenuBlockSchema = BaseSchema.extend({ * Union of the public-block arms — the category member `AnyComponentSchema` * lists (objectui#10872). */ -export const PublicBlockComponentSchema = z.discriminatedUnion('type', [ +const PublicBlockComponentSchemaInferred = z.discriminatedUnion('type', [ PageHeaderBlockSchema, PageTabsBlockSchema, PageCardBlockSchema, @@ -1584,3 +1586,16 @@ export const PublicBlockComponentSchema = z.discriminatedUnion('type', [ ActionGroupBlockSchema, ActionMenuBlockSchema, ]); + +/** + * The TYPE of {@link PublicBlockComponentSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface PublicBlockComponentZodType extends PublicBlockComponentSchemaInferredType { + options: PublicBlockComponentSchemaInferredType['options']; +} +type PublicBlockComponentSchemaInferredType = typeof PublicBlockComponentSchemaInferred; + +/** The union above, typed by its named {@link PublicBlockComponentZodType}. */ +export const PublicBlockComponentSchema: PublicBlockComponentZodType = PublicBlockComponentSchemaInferred; diff --git a/packages/types/src/zod/reports.zod.ts b/packages/types/src/zod/reports.zod.ts index b635d5a0cb..5641fb02ec 100644 --- a/packages/types/src/zod/reports.zod.ts +++ b/packages/types/src/zod/reports.zod.ts @@ -344,7 +344,7 @@ export type ReportNodeZodType = z.ZodObject< /** * Union of all report schemas */ -export const ReportUnionSchema = z.discriminatedUnion('type', [ +const ReportUnionSchemaInferred = z.discriminatedUnion('type', [ // objectui#11440 — the authored `report` node is `ReportNodeSchema` (the // record plus the `report` wrapper); `ReportComponentSchema` stays the record. ReportNodeSchema, @@ -352,6 +352,19 @@ export const ReportUnionSchema = z.discriminatedUnion('type', [ ReportViewerSchema, ]); +/** + * The TYPE of {@link ReportUnionSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface ReportUnionZodType extends ReportUnionSchemaInferredType { + options: ReportUnionSchemaInferredType['options']; +} +type ReportUnionSchemaInferredType = typeof ReportUnionSchemaInferred; + +/** The union above, typed by its named {@link ReportUnionZodType}. */ +export const ReportUnionSchema: ReportUnionZodType = ReportUnionSchemaInferred; + /** * Export type inference helpers */ diff --git a/packages/types/src/zod/views.zod.ts b/packages/types/src/zod/views.zod.ts index 9d30d9eb60..0683858a54 100644 --- a/packages/types/src/zod/views.zod.ts +++ b/packages/types/src/zod/views.zod.ts @@ -569,7 +569,7 @@ export const DetailSectionNodeSchema = BaseSchema.extend({ /** * Union of all view schemas */ -export const ViewComponentSchema = z.discriminatedUnion('type', [ +const ViewComponentSchemaInferred = z.discriminatedUnion('type', [ DetailViewSchema, ViewSwitcherSchema, FilterUISchema, @@ -578,6 +578,19 @@ export const ViewComponentSchema = z.discriminatedUnion('type', [ DetailSectionNodeSchema, ]); +/** + * The TYPE of {@link ViewComponentSchema}, NAMED so declaration emit prints it by + * reference (objectui#11573): see "Why every category union's TYPE is named" + * on `AnyComponentSchema` (`index.zod.ts`). It adds no member. + */ +export interface ViewComponentZodType extends ViewComponentSchemaInferredType { + options: ViewComponentSchemaInferredType['options']; +} +type ViewComponentSchemaInferredType = typeof ViewComponentSchemaInferred; + +/** The union above, typed by its named {@link ViewComponentZodType}. */ +export const ViewComponentSchema: ViewComponentZodType = ViewComponentSchemaInferred; + /** * Export type inference helpers */