diff --git a/.changeset/8885-object-chart-block-props.md b/.changeset/8885-object-chart-block-props.md new file mode 100644 index 0000000000..6aa59548f1 --- /dev/null +++ b/.changeset/8885-object-chart-block-props.md @@ -0,0 +1,22 @@ +--- +'@object-ui/plugin-charts': minor +--- + +**BREAKING** — `ObjectChartBlock`, the registry shell exported beside `ObjectChart`, declares its props instead of taking `(props: any)` (objectui#8885). + +**Clause-②: yes** — a published component signature narrows. `@object-ui/plugin-charts` 17.6.0 publishes `ObjectChartBlock: (props: any) => React.JSX.Element`. It is now `(props: Omit & { schema: BaseSchema }) => React.JSX.Element`. No export is added, removed or renamed, and no runtime behaviour changes. + +**FROM** any props bag **TO** `ObjectChart`'s own props, with `schema` as the raw node: + +```tsx +// before: all of these compiled + // misspelled prop, silently dropped + // a string, not a node + // a node with no `type` +// after: pass a node (`BaseSchema`: at least `type`) and only ObjectChart's props + +``` + +`schema` is `BaseSchema`, the node `SchemaRenderer` hands every registered renderer, and not `ObjectChartSchema`. The shell receives the node BEFORE `ElementDataSourceGate` maps its `dataSource` binding, so a node that carries only `{ type, dataSource: { object, view } }` is legal input. So is a node typed `chart`, the alias this same shell is registered under. The other props (`dataSource`, `onSegmentClick`) are `ObjectChartProps`' own, so a misspelled prop name is now an excess-property error. + +Migration: a direct caller that passed extra props through the shell must pass only `schema`, `dataSource` and `onSegmentClick`, and a node without `type` must add one. Nodes rendered through the registry (`SchemaRenderer`) are unaffected: the registry resolves renderers as `any`, and the type annotations and the single hand-off cast erase at build. diff --git a/packages/plugin-charts/src/ObjectChart.tsx b/packages/plugin-charts/src/ObjectChart.tsx index 1f26145df6..6ad6d4e17f 100644 --- a/packages/plugin-charts/src/ObjectChart.tsx +++ b/packages/plugin-charts/src/ObjectChart.tsx @@ -7,7 +7,7 @@ import { ComponentRegistry, chartMeasureKey, isStructuredGroupBy, objectAggregat import { Sheet, SheetContent, SheetHeader, SheetTitle, Dialog, DialogContent, DialogHeader, DialogTitle, RefreshIndicator, Button, ChartSkeleton, DataEmptyState } from '@object-ui/components'; import { AlertCircle, ArrowUpRight, Inbox } from 'lucide-react'; import { builtinAggregateLabels, useSafeFieldLabel, useSafeTranslate, useObjectTranslation, pickLocalized } from '@object-ui/i18n'; -import type { DrillDownConfig, ObjectChartSchema } from '@object-ui/types'; +import type { BaseSchema, DrillDownConfig, ObjectChartSchema } from '@object-ui/types'; /** * Humanize a snake_case or kebab-case string into Title Case. @@ -1546,18 +1546,44 @@ const OBJECT_CHART_DATA_SOURCE: ElementDataSourceMapping = { * spec documents rendered an empty frame with no error and no request. Lives * here, beside the registration, rather than in `ChartContainerImpl` — the * binding is a registry-boundary concern, not a rendering one. + * + * ## Its props type — the node BEFORE the gate (objectui#8885) + * + * This shell used to be published as `(props: any)`. `schema` is now + * `BaseSchema`, the node `SchemaRenderer` hands every registered renderer, and + * ⛔ NOT `ObjectChartSchema`. The node arrives here before the gate has mapped + * its `dataSource` binding, and nothing has validated it as a chart. Typing it + * post-gate would claim a validation that has not happened, and that type's + * `type: 'object-chart'` literal is also false for the `chart` alias that + * `index.tsx` registers this same shell under. + * + * Every other prop is `ObjectChart`'s own, taken from `ObjectChartProps`, so + * the type is CLOSED: a misspelled prop name is an excess-property error, not + * an `any` passed straight through. Both halves are existing exported names; + * no new type is minted. Pinned by + * `__tests__/ObjectChartBlock.props-8885.test.tsx`. */ -export const ObjectChartBlock = elementDataSourceBlock((props: any) => ( - - {(bound) => } - -)); +export const ObjectChartBlock = elementDataSourceBlock( + (props: Omit & { schema: BaseSchema }) => ( + + {(bound) => ( + // The ONE loose member of this signature, kept on purpose. This is + // where the pre-gate node becomes the component's post-gate schema, + // and `ObjectChart` is where the post-gate claim belongs. The gate + // maps the binding and does not validate, so the claim has to be + // made here, at one line, instead of on the published signature + // above. + + )} + + ), +); // Register it ComponentRegistry.register('object-chart', ObjectChartBlock, { diff --git a/packages/plugin-charts/src/__tests__/ObjectChartBlock.props-8885.test.tsx b/packages/plugin-charts/src/__tests__/ObjectChartBlock.props-8885.test.tsx new file mode 100644 index 0000000000..d8eaf01a96 --- /dev/null +++ b/packages/plugin-charts/src/__tests__/ObjectChartBlock.props-8885.test.tsx @@ -0,0 +1,111 @@ +/** + * 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. + */ + +/** + * objectui#8885 Part 2: `ObjectChartBlock`, the registry shell around + * `ObjectChart`, gets a declared props type instead of `(props: any)`. + * + * Ruling 5810232641 (maintainer 「同意」), following the objectui#7912 / + * objectui#7483 direction: a published component signature is never bare + * `any`. The shell receives the node BEFORE `ElementDataSourceGate`, so its + * `schema` is the registry renderer's raw node input, `BaseSchema`, and ⛔ not + * the post-gate `ObjectChartSchema`. Every other prop is `ObjectChart`'s own, + * so the type is closed. + * + * ## Why this pin is compile-time + * + * Nothing renders differently. What moved is the ACCEPT SET of a published + * signature, and `tsconfig.test.json` compiles this file, so every statement + * below is enforcement: a `@ts-expect-error` whose refusal stops happening + * fails the build with TS2578. The shell is taken as a TYPE (`import type` plus + * `declare const`), so no module side effect runs here and nothing below is + * ever rendered. The probes live in a function that is never called. + * + * ## The ceiling, stated rather than assumed + * + * A closed PROPS type refuses a misspelled prop NAME. It does not refuse a + * misspelled key INSIDE the node: `BaseSchema` carries `[key: string]: any`, + * the protocol's passthrough. The counter-probe at the bottom keeps that + * visible so nobody reads this pin as more than it is. + */ + +import { describe, it, expect } from 'vitest'; +import type * as ObjectChartModule from '../ObjectChart'; +import type { ObjectChartProps } from '../ObjectChart'; +import type { BaseSchema, ObjectChartSchema } from '@object-ui/types'; + +declare const ObjectChartBlock: typeof ObjectChartModule.ObjectChartBlock; +declare const adapter: unknown; + +type Equal = + (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +type Expect = T; +type IsAny = 0 extends 1 & T ? true : false; + +/** What the published shell declares it accepts. */ +type ShellProps = Parameters[0]; + +// ── The any-guard: the regression this card exists to stop ───────────────── +export type assertionShellParamIsNotAny = Expect, false>>; +export type assertionShellSchemaIsNotAny = Expect, false>>; + +// ── The ruling's letter: pre-gate `BaseSchema`, never the post-gate type ──── +export type assertionSchemaIsTheRawNode = Expect>; +export type assertionSchemaIsNotPostGate = Expect, false>>; +/** The rest is `ObjectChart`'s own props, nothing more and nothing less. */ +export type assertionRestIsObjectChartProps = Expect< + Equal, Omit> +>; + +// ── The helpers can FAIL, so a vacuous `Equal` / `IsAny` cannot pass this file ─ +export type assertionEqualCanFail = Expect, false>>; +export type assertionAnyProbeCanFire = Expect, true>>; + +/** Never called: compiled by `tsc -p tsconfig.test.json`, never rendered. */ +const probes = () => ( + <> + {/* P1: a misspelled prop NAME (`dataSource` spelled `dataSorce`). */} + {/* @ts-expect-error — the props type is closed, so the misspelling is an excess property, not an `any` passed through. */} + + {/* P2: `schema` itself misspelled, so the required prop is missing. */} + {/* @ts-expect-error — `schema` is required. */} + + {/* P3: a string where the node belongs. */} + {/* @ts-expect-error — `schema` is a node (`BaseSchema`); a bare string is not one. */} + + {/* P4: a node without `type`. */} + {/* @ts-expect-error — `type` is required on every node; it is the registry key. */} + + + {/* C1 control: the `chart` alias registration's node. Refused by the + post-gate `ObjectChartSchema` (whose `type` is the literal + 'object-chart'), and it must compile here: `index.tsx` registers this + same shell under `chart`. */} + + {/* C2 control: a pre-gate node that carries only the spec's binding. The + gate maps `dataSource.object` onto `objectName`, so the node is legal + input before it has any `objectName` or `chartType` of its own. */} + + {/* C3 control: `ObjectChart`'s own props still pass straight through the shell. */} + undefined} + /> + + {/* CEILING, not a refusal: a misspelled key INSIDE the node still + compiles, through `BaseSchema`'s passthrough index signature. */} + + +); + +describe('ObjectChartBlock props: the pre-gate node, closed (objectui#8885)', () => { + it('is a compile-time pin; the probes type-check under tsconfig.test.json and are never rendered', () => { + expect(typeof probes).toBe('function'); + }); +});