From c3e363b5cae97529e8fec987111fb0db5ca0ff0e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 17:01:07 +0000 Subject: [PATCH 1/3] fix(react)!: dataSource at the renderer seam is the published DataSource contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SchemaRendererProvider`'s `dataSource` prop and the context type every `useSchemaContext()` consumer reads back were both declared `any`, so a bare string passed where the host's adapter belongs raised nothing at compile time and failed at runtime on the first `find()`. Both are now `DataSource | null | undefined` — the spelling `useSettledSchema` in this same package already used — so the two absences that really occur stay expressible and everything that is not an adapter is refused. Red call sites are repaired by typing, never by casting at the seam: - `useClientNotifications` declares `getClient` as the adapter CAPABILITY it is and probes it with a type guard, instead of reaching through `any`. - `useDataScope`'s path walk annotates its accumulator; its published return type is unchanged. - `ObjectGallery` drops a hand-rolled `{ find }` stand-in for the real contract, and the now-redundant cast at its `useSettledSchema` call. - the `kind:'react'` page stops passing a truthy empty object as its "no adapter yet" stand-in, so the `if (!dataSource)` guards written for that state fire; the memo identity the module constant existed for is kept, because the absent adapter is itself render-stable. - calendar, gantt and kanban collapse a null context adapter to the single spelling their widget prop declares. - five redundant `as any` reads of this context in `@object-ui/fields` are gone, along with `LookupField`'s local re-declaration of the imported context as a `Context` of `any`, which laundered its `dataSource` read while looking typed. Test doubles keep every injected value byte-for-byte and mark the crossing, so no assertion moves: a double that stubs only the members its path calls is not a host, and completing them would change which capability probes fire. Both directions are pinned against the real compiler, with controls that fire if the harness ever stops resolving the real types. The documentation probe from the card — a bare string in this package's README provider example — now fails `check:doc-snippets`, where it used to exit 0 with zero diagnostics; the same gate caught the quick-start guide teaching an empty object, which is repaired. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ --- ...912-schema-renderer-datasource-contract.md | 87 ++++++ content/docs/guide/quick-start.md | 8 +- .../bindable-text-keys-4795.test.tsx | 15 +- .../data-table-node-data-diagnostic.test.tsx | 15 +- .../skill-guide-data-table-binding.test.tsx | 15 +- .../skill-guide-provider-envelope.test.tsx | 15 +- .../shadowed-renderer-behaviour.test.tsx | 15 +- .../action-enablement-cel-envelope.test.tsx | 15 +- .../src/renderers/layout/react-page.tsx | 29 +- packages/fields/src/index.tsx | 4 +- .../widgets/CapabilityMultiSelectField.tsx | 5 +- .../src/widgets/FilterConditionField.tsx | 4 +- packages/fields/src/widgets/LookupField.tsx | 23 +- .../fields/src/widgets/ObjectRefField.tsx | 6 +- .../src/widgets/RecipientPickerField.tsx | 4 +- packages/plugin-calendar/src/index.tsx | 8 +- .../plugin-calendar/src/registration.test.tsx | 7 +- .../src/ObjectChart.optionColors.test.tsx | 19 +- ...shboardChart.categoryAxisKey-8269.test.tsx | 15 +- .../DashboardChart.chartConfig-4044.test.tsx | 15 +- ...ashboardChart.chartConfigDom-4044.test.tsx | 15 +- ...hboardChart.chartConfigMarks-4044.test.tsx | 15 +- ...ashboardChart.countSeriesKey-8266.test.tsx | 15 +- .../__tests__/DatasetWidget.relabel.test.tsx | 17 +- ...ctDataTable.bindNotForwarded-6575.test.tsx | 15 +- .../src/useRecordEditable.test.tsx | 14 +- packages/plugin-gantt/src/index.tsx | 8 +- packages/plugin-kanban/src/index.tsx | 8 +- .../plugin-kanban/src/registration.test.tsx | 7 +- packages/plugin-list/src/ObjectGallery.tsx | 47 +++- .../ListView.ariaLabelInlineLocale.test.tsx | 16 +- .../ListView.density.offspec.test.tsx | 16 +- ...View.descriptionInlineLocale-7199.test.tsx | 16 +- .../ListView.overlayTitleI18n.test.tsx | 18 +- ...ew.overlayTitleNoProviderFallback.test.tsx | 16 +- .../src/__tests__/ListView.test.tsx | 16 +- .../src/__tests__/UserFilters.addTab.test.tsx | 15 +- packages/react/README.md | 10 + .../__tests__/SchemaRenderer.debug.test.tsx | 18 +- ...hemaRenderer.disabledDeclaredGate.test.tsx | 15 +- .../SchemaRenderer.expressions.test.tsx | 31 ++- ...SchemaRenderer.hiddenDeclaredGate.test.tsx | 18 +- ...nderer.predicateEnvelopeConfigBag.test.tsx | 15 +- ...enderer.predicateEnvelopeDeclared.test.tsx | 16 +- .../SchemaRenderer.validation.test.tsx | 22 +- .../SchemaRendererProvider.smoke.test.tsx | 30 +- .../src/context/SchemaRendererContext.tsx | 47 +++- ...RendererContext.dataSourceType.pin.test.ts | 260 ++++++++++++++++++ .../context/__tests__/useDataScope.test.tsx | 24 +- .../react/src/hooks/useClientNotifications.ts | 20 +- 50 files changed, 1015 insertions(+), 109 deletions(-) create mode 100644 .changeset/7912-schema-renderer-datasource-contract.md create mode 100644 packages/react/src/context/__tests__/SchemaRendererContext.dataSourceType.pin.test.ts diff --git a/.changeset/7912-schema-renderer-datasource-contract.md b/.changeset/7912-schema-renderer-datasource-contract.md new file mode 100644 index 0000000000..76a60c4b77 --- /dev/null +++ b/.changeset/7912-schema-renderer-datasource-contract.md @@ -0,0 +1,87 @@ +--- +'@object-ui/react': minor +'@object-ui/components': minor +'@object-ui/fields': minor +'@object-ui/plugin-list': minor +'@object-ui/plugin-calendar': minor +'@object-ui/plugin-gantt': minor +'@object-ui/plugin-kanban': minor +'@object-ui/plugin-charts': minor +'@object-ui/plugin-dashboard': minor +'@object-ui/plugin-detail': minor +--- + +**BREAKING** — `SchemaRendererProvider`'s `dataSource` prop, and the context +type every `useSchemaContext()` consumer reads back, are the published +`DataSource` contract instead of `any`. + +**FROM** `dataSource={anything}` **TO** `dataSource={adapter}` — a `DataSource` +from `@object-ui/types`, or `null` / `undefined` when the host has no adapter +bound. + +```ts +// before — compiled, and failed at runtime on the first find() + +// before — compiled, and no reader can do anything with it + +// after + // a DataSource + // "I have no adapter" +``` + +Both sites are typed `DataSource | null | undefined` — the spelling +`useSettledSchema` in this same package already used. The two absences are part +of the contract, not a weakening of it: a Studio preview, a `kind:'react'` page +rendered before the host's adapter connects, and a widget probe driving +`apiFetch` alone all render with nothing bound, and every reader in the tree +already guards for it. What the union refuses is everything that is not an +adapter: a string, an empty object, a plain data bag, a partial adapter missing +a required member. + +The measured cost, both halves, because the two `any`s have different blast +radii (measured separately on `origin/main`, whole-repo type-check over the 33 +packages that depend on `@object-ui/react`): + +- the **context type** — the `any` that reaches every `useSchemaContext()` + reader — reds **7 diagnostics at 7 sites in 4 packages**, all of them + production code or a mocked module factory. +- the **provider prop** — the injection points — reds **52 diagnostics at 27 + sites in 11 packages**, all but one of them test doubles. + +That ordering is the reverse of the prediction on the card: the context `any` +was expected to be the expensive one because it infects the whole tree, and it +is the cheap one, because every reader in the tree already guarded and none of +them ever reached past `find` / `getObjectSchema`. The prop is the expensive +one, because the injection points are overwhelmingly test doubles that were +never complete adapters. The full accounting is on objectui#7912. + +Two runtime behaviours change, both in the "no adapter" direction and both +strictly closer to what the surrounding code already intended: + +- `@object-ui/components`' `kind:'react'` page passed an empty object as its + "no adapter yet" stand-in. An empty object is TRUTHY, so it walked past every + `if (!dataSource)` guard written to catch exactly that state and failed later, + at the call. It now passes the absent adapter itself, so the guard fires where + it was meant to. The module-constant identity that stand-in existed for is + preserved: `null` is a primitive, so the provider's memo is unaffected. +- `@object-ui/plugin-calendar`, `@object-ui/plugin-gantt` and + `@object-ui/plugin-kanban` collapse a `null` adapter from the context to + `undefined` before handing it to their widget, whose prop declares the single + spelling `dataSource?: DataSource`. + +Nothing else moves at runtime: no value flowing through this key changes, and +no data path is touched. A TypeScript consumer outside this repo that handed +this prop something other than an adapter now gets a compile error naming the +key (TS2322 / TS2739 / TS2740), which is why the FROM/TO is spelled out above. + +Five `as any` reads of this context in `@object-ui/fields` are gone — they were +redundant the moment the seam became honest — and `LookupField`'s local +re-declaration of the imported context as a `Context` of `any`, which laundered +its `dataSource` read while looking typed, is gone with them. Both directions of +the contract are pinned against the real compiler in +`SchemaRendererContext.dataSourceType.pin.test.ts`, and the card's planted +documentation probe (a bare string in `packages/react/README.md`'s provider +example) now fails `pnpm check:doc-snippets`, where it used to exit 0 with zero +diagnostics. + +objectui#7912. diff --git a/content/docs/guide/quick-start.md b/content/docs/guide/quick-start.md index fcdfeef315..915f89758b 100644 --- a/content/docs/guide/quick-start.md +++ b/content/docs/guide/quick-start.md @@ -109,7 +109,7 @@ const schema: CardSchema = { function App() { return (
- +
@@ -119,7 +119,9 @@ function App() { export default App; ``` -Importing `@object-ui/components` and `@object-ui/fields` registers their renderers with the shared `ComponentRegistry`. `SchemaRendererProvider` supplies the data scope used by expressions, smart fields, and data-aware plugins. +Importing `@object-ui/components` and `@object-ui/fields` registers their renderers with the shared `ComponentRegistry`. `SchemaRendererProvider` injects the host's data adapter, and everything below it — expressions, smart fields, data-aware plugins — reads it back from there. + +This app renders inline `data`, so it has no adapter to inject and says so with `undefined`. That is a real state of the contract, not a placeholder: `dataSource` is typed `DataSource | null | undefined` (`@object-ui/types`), so a host either hands over an adapter or states that it has none. It used to be typed `any` and this example passed an empty object, which no renderer can do anything with (objectui#7912). ## Step 5: Run the App @@ -134,7 +136,7 @@ Open [http://localhost:5173](http://localhost:5173). You should see a card and d 1. **Schema** - the UI was described as JSON with `type`, visual props, and nested `body`. 2. **Registry** - importing the component packages registered renderers for `card` and `data-table`. 3. **Renderer** - `SchemaRenderer` resolved each `type` and rendered React components. -4. **Provider** - `SchemaRendererProvider` made a data scope available for expressions and plugins. +4. **Provider** - `SchemaRendererProvider` is where a host injects its `DataSource`; this app has none, so it passes `undefined`. ## Next Steps diff --git a/packages/components/src/__tests__/bindable-text-keys-4795.test.tsx b/packages/components/src/__tests__/bindable-text-keys-4795.test.tsx index 456ce07d52..47bc4d6759 100644 --- a/packages/components/src/__tests__/bindable-text-keys-4795.test.tsx +++ b/packages/components/src/__tests__/bindable-text-keys-4795.test.tsx @@ -37,12 +37,25 @@ import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; // Module scope, not a hook — the cold transform would otherwise be billed to // `hookTimeout` (object-ui/no-dynamic-import-in-test-hook, objectui#3010). import '../renderers'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ const DATA = { total: 99, caption: 'Active users', note: '+20.1% from last month' }; const renderNode = (schema: any) => render( - + , ); diff --git a/packages/components/src/__tests__/data-table-node-data-diagnostic.test.tsx b/packages/components/src/__tests__/data-table-node-data-diagnostic.test.tsx index 6a3aab752e..9f0dadf407 100644 --- a/packages/components/src/__tests__/data-table-node-data-diagnostic.test.tsx +++ b/packages/components/src/__tests__/data-table-node-data-diagnostic.test.tsx @@ -51,9 +51,22 @@ import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; // self-import (`scripts/check-package-self-import.mjs`). import '../renderers'; import { + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ DATA_TABLE_BIND_DIAGNOSTIC_PREFIX, DATA_TABLE_DATA_DIAGNOSTIC_PREFIX, } from '../renderers/complex/dataTableBindDiagnostic'; +import type { DataSource } from '@object-ui/types'; /** Identical in every leg, so the only variable is where `data` was written. */ const COLUMNS = [ @@ -101,7 +114,7 @@ function warningsOn(prefix: string): string[] { function tree(schema: unknown) { return ( - + ); diff --git a/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx b/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx index 76fdb2814e..ad853f790a 100644 --- a/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx +++ b/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx @@ -75,6 +75,19 @@ import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; // package self-import (`scripts/check-package-self-import.mjs`). import '../renderers'; import { DATA_TABLE_BIND_DIAGNOSTIC_PREFIX } from '../renderers/complex/dataTableBindDiagnostic'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ const here = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(here, '../../../..'); @@ -152,7 +165,7 @@ function bindWarnings(): string[] { function renderNode(schema: unknown, dataSource: unknown) { return render( - + , ); diff --git a/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx b/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx index 24a53b6ed4..ff32965d56 100644 --- a/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx +++ b/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx @@ -65,6 +65,19 @@ import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; // specifier: this file lives inside `@object-ui/components` // (`scripts/check-package-self-import.mjs`). import '../renderers'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ const here = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(here, '../../../..'); @@ -90,7 +103,7 @@ const EMPTY_STATE = 'No results foundTry adjusting your filters or search query. function renderNode(schema: unknown) { return render( - + , ); diff --git a/packages/components/src/renderers/__tests__/shadowed-renderer-behaviour.test.tsx b/packages/components/src/renderers/__tests__/shadowed-renderer-behaviour.test.tsx index 46c09cdcb9..da7c526527 100644 --- a/packages/components/src/renderers/__tests__/shadowed-renderer-behaviour.test.tsx +++ b/packages/components/src/renderers/__tests__/shadowed-renderer-behaviour.test.tsx @@ -32,6 +32,19 @@ import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; // Module-scope side-effect import, not a `beforeAll` — see // object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). import '../index'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ const PEOPLE = [ { name: 'Ada', role: 'Engineer' }, @@ -59,7 +72,7 @@ const DATA_SOURCE = { const renderBound = (schema: Record) => render( - + , ); diff --git a/packages/components/src/renderers/action/__tests__/action-enablement-cel-envelope.test.tsx b/packages/components/src/renderers/action/__tests__/action-enablement-cel-envelope.test.tsx index fe5628c443..8a46d19de2 100644 --- a/packages/components/src/renderers/action/__tests__/action-enablement-cel-envelope.test.tsx +++ b/packages/components/src/renderers/action/__tests__/action-enablement-cel-envelope.test.tsx @@ -58,6 +58,19 @@ import { SchemaRenderer, SchemaRendererProvider, PredicateScopeProvider } from ' // the import phase, not under a hook timeout. import '../action-button'; import '../action-icon'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ const DATA = { status: 'draft' }; @@ -67,7 +80,7 @@ const FAILS = { dialect: 'cel', source: 'has(data.status) && data.status == "pub function mountAction(type: 'action:button' | 'action:icon', properties: Record) { return render( - + = ({ schema }) => { const { ReactRunner } = runtime; return ( - + ) { const { t } = useFieldTranslation(); const ctx = React.useContext(SchemaRendererContext); + // The context leg needs no cast since objectui#7912 typed the seam; the + // `props` leg still does, because `FieldWidgetProps.dataSource` is declared + // `unknown` in this package. const dataSource: DataSource | null = - (props.dataSource as any) ?? (ctx as any)?.dataSource ?? null; + (props.dataSource as DataSource | null | undefined) ?? ctx?.dataSource ?? null; const disabled = props.disabled; const [caps, setCaps] = React.useState(null); diff --git a/packages/fields/src/widgets/FilterConditionField.tsx b/packages/fields/src/widgets/FilterConditionField.tsx index 09e6e5c4e0..ad9b51a859 100644 --- a/packages/fields/src/widgets/FilterConditionField.tsx +++ b/packages/fields/src/widgets/FilterConditionField.tsx @@ -429,7 +429,9 @@ export function FilterConditionField({ }: FieldWidgetComponentProps) { const ctx = React.useContext(SchemaRendererContext); const { t } = useFieldTranslation(); - const dataSource: any = props.dataSource ?? (ctx as any)?.dataSource ?? null; + // Cast-free context read (objectui#7912); the local stays `any` for the + // `FieldWidgetProps.dataSource?: unknown` channel it merges with. + const dataSource: any = props.dataSource ?? ctx?.dataSource ?? null; const dependentValues: Record = (props as any).dependentValues ?? {}; const objectName = String(dependentValues.object_name ?? ''); diff --git a/packages/fields/src/widgets/LookupField.tsx b/packages/fields/src/widgets/LookupField.tsx index e9d573ce58..11fa2f3eec 100644 --- a/packages/fields/src/widgets/LookupField.tsx +++ b/packages/fields/src/widgets/LookupField.tsx @@ -56,8 +56,17 @@ const LOOKUP_PAGE_SIZE = 50; /** * SchemaRendererContext is created by @object-ui/react. * Using a static import to be compatible with Next.js Turbopack SSR. + * + * ⚠️ This used to re-declare the imported context as `React.Context`. + * That widening was invisible at the read sites — every `ctx?.…` below read as + * `any` while looking perfectly typed — so it also laundered the `dataSource` + * read, which is precisely the consumer face objectui#7912 typed. The import + * is now used AS DECLARED; the one read that needs members the context does + * not declare takes a local widened VIEW of the value (see the + * `resolvedDependentValues` note), so the widening is visible where it happens + * and reaches nothing else. */ -const SchemaRendererContext: React.Context = ImportedSchemaRendererContext; +const SchemaRendererContext = ImportedSchemaRendererContext; /** * A relation whose picker should offer inline "create the referenced record" by @@ -354,6 +363,14 @@ export function LookupField({ value, onChange, field, readonly, error: fieldErro // Resolve DataSource: explicit prop > field-level > wrapper field > SchemaRendererContext > none const ctx = useContext(SchemaRendererContext); const contextDataSource = ctx?.dataSource ?? null; + /** A deliberately widened VIEW of the same context value, for the two reads + * below that name members `SchemaRendererContextType` does not declare. It + * exists so that widening cannot reach the `dataSource` read above; the + * reads themselves are unchanged, and objectui#7206 still owns whether that + * channel becomes real or is retired. */ + const untypedCtx = ctx as unknown as + | { formValues?: Record; data?: Record } + | null; const dataSource: DataSource | null = (props.dataSource as DataSource | null | undefined) ?? lookupField?.dataSource ?? fieldMeta?.dataSource ?? contextDataSource; @@ -380,8 +397,8 @@ export function LookupField({ value, onChange, field, readonly, error: fieldErro * read this note as either outcome. */ const resolvedDependentValues: Record = useMemo(() => { if (dependentValuesProp) return dependentValuesProp; - return (ctx?.formValues ?? ctx?.data ?? {}) as Record; - }, [dependentValuesProp, ctx?.formValues, ctx?.data]); + return (untypedCtx?.formValues ?? untypedCtx?.data ?? {}) as Record; + }, [dependentValuesProp, untypedCtx?.formValues, untypedCtx?.data]); /** True when at least one dependency is missing (empty). The picker is gated * in that state so we never issue an unfiltered query that ignores the diff --git a/packages/fields/src/widgets/ObjectRefField.tsx b/packages/fields/src/widgets/ObjectRefField.tsx index e4c21678f5..d07261113d 100644 --- a/packages/fields/src/widgets/ObjectRefField.tsx +++ b/packages/fields/src/widgets/ObjectRefField.tsx @@ -37,7 +37,11 @@ export function ObjectRefField({ }: FieldWidgetComponentProps) { const ctx = React.useContext(SchemaRendererContext); const { t } = useFieldTranslation(); - const dataSource: any = props.dataSource ?? (ctx as any)?.dataSource ?? null; + // The context read is cast-free since objectui#7912 typed the seam. The + // local stays `any` because the OTHER channel, `FieldWidgetProps.dataSource`, + // is declared `unknown` in this package — that is the declaration that + // launders the merged value now, not the renderer context. + const dataSource: any = props.dataSource ?? ctx?.dataSource ?? null; const disabled = props.disabled; const [objects, setObjects] = React.useState(null); diff --git a/packages/fields/src/widgets/RecipientPickerField.tsx b/packages/fields/src/widgets/RecipientPickerField.tsx index e15e6b4a0e..062ac5b308 100644 --- a/packages/fields/src/widgets/RecipientPickerField.tsx +++ b/packages/fields/src/widgets/RecipientPickerField.tsx @@ -59,7 +59,9 @@ export function RecipientPickerField({ }: FieldWidgetComponentProps) { const ctx = React.useContext(SchemaRendererContext); const { t } = useFieldTranslation(); - const dataSource: any = props.dataSource ?? (ctx as any)?.dataSource ?? null; + // Cast-free context read (objectui#7912); the local stays `any` for the + // `FieldWidgetProps.dataSource?: unknown` channel it merges with. + const dataSource: any = props.dataSource ?? ctx?.dataSource ?? null; const disabled = props.disabled; const dependentValues: Record = (props as any).dependentValues ?? {}; const recipientType = String(dependentValues.recipient_type ?? ''); diff --git a/packages/plugin-calendar/src/index.tsx b/packages/plugin-calendar/src/index.tsx index ac9230f3f5..0458024195 100644 --- a/packages/plugin-calendar/src/index.tsx +++ b/packages/plugin-calendar/src/index.tsx @@ -247,7 +247,13 @@ export const ObjectCalendarRenderer: React.FC<{ schema: any; [key: string]: any // reason an authored string could land on a function-typed prop. ...rest }) => { - const { dataSource } = useSchemaContext() || {}; + // `useSchemaContext()` may hand back a NULL adapter: a host with nothing + // bound spells absence either way, and the seam declares both + // (`DataSource | null | undefined`, objectui#7912). The widget below + // declares the single spelling `dataSource?: DataSource`, so collapse the + // two absences into that one here rather than widening the widget. + const { dataSource: contextDataSource } = useSchemaContext() || {}; + const dataSource = contextDataSource ?? undefined; // The declared host hatches, each kept only at its declared type. Read out of // `rest`; `rest` itself never reaches `ObjectCalendar`. diff --git a/packages/plugin-calendar/src/registration.test.tsx b/packages/plugin-calendar/src/registration.test.tsx index e00765d280..3388e0c592 100644 --- a/packages/plugin-calendar/src/registration.test.tsx +++ b/packages/plugin-calendar/src/registration.test.tsx @@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import * as ObjectUIReact from '@object-ui/react'; import { ObjectCalendarRenderer } from './index'; +import type { DataSource } from '@object-ui/types'; // Partial mock — override ONLY what this test controls, keep every other real // export (objectui#3219). @@ -38,7 +39,11 @@ import { ObjectCalendarRenderer } from './index'; vi.mock(import('@object-ui/react'), async (importOriginal) => ({ ...(await importOriginal()), // Only the pieces this test drives: - useSchemaContext: vi.fn(() => ({ dataSource: { type: 'mock-datasource' } })), + // The marker object below is NOT an adapter: the stubbed widget prints + // `dataSource.type`, which is the whole point of this registration probe. + // `useSchemaContext` declares the published `DataSource` contract since + // objectui#7912, so the crossing is explicit; the value is unchanged. + useSchemaContext: vi.fn(() => ({ dataSource: { type: 'mock-datasource' } as unknown as DataSource })), })); // Mock the implementation. Deliberate whole-module replacement of a LOCAL diff --git a/packages/plugin-charts/src/ObjectChart.optionColors.test.tsx b/packages/plugin-charts/src/ObjectChart.optionColors.test.tsx index 1da3a6ba65..8c2ec7214d 100644 --- a/packages/plugin-charts/src/ObjectChart.optionColors.test.tsx +++ b/packages/plugin-charts/src/ObjectChart.optionColors.test.tsx @@ -22,6 +22,7 @@ * right documents, in the right order, and feeds the results to the renderer). */ +import type { DataSource } from '@object-ui/types'; import React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, cleanup, waitFor } from '@testing-library/react'; @@ -38,6 +39,18 @@ vi.mock('./ChartRenderer', () => ({ import { ObjectChart } from './ObjectChart'; +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ + /** * Records every URL and answers only the documents it is given, so an escape * to any other endpoint shows up as a recorded call the assertions reject — @@ -268,7 +281,7 @@ describe('ObjectChart — option-color probe routing (objectui#4114)', () => { render( ({ data: [] }) }} + dataSource={{ find: async () => ({ data: [] }) } as unknown as DataSource} apiFetch={host.fn} > @@ -321,7 +334,7 @@ describe('ObjectChart — option-color probe routing (objectui#4114)', () => { rows: [{ status: 'todo', task_count: 3 }], fields: [{ name: 'status', label: 'Status' }, { name: 'task_count', label: 'Tasks' }], }), - }} + } as unknown as DataSource} apiFetch={host.fn} > { const globalCalls = installMetaFetchDouble(OPPORTUNITY_DOC); render( - ({ data: [] }) }}> + ({ data: [] }) } as unknown as DataSource}> , ); diff --git a/packages/plugin-dashboard/src/__tests__/DashboardChart.categoryAxisKey-8269.test.tsx b/packages/plugin-dashboard/src/__tests__/DashboardChart.categoryAxisKey-8269.test.tsx index b431156506..ed7b04d872 100644 --- a/packages/plugin-dashboard/src/__tests__/DashboardChart.categoryAxisKey-8269.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/DashboardChart.categoryAxisKey-8269.test.tsx @@ -70,6 +70,19 @@ import '@object-ui/components'; import '@object-ui/plugin-charts'; import '../index'; import { DashboardRenderer } from '../DashboardRenderer'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ /** Every composed chart node the relays handed the renderer, in order. */ const composed: any[] = []; @@ -94,7 +107,7 @@ const dataSource = { aggregate: async () => [], find: async () => [] }; const composeVia = async (surface: 'grid' | 'renderer', widget: Record) => { composed.length = 0; render( - + {surface === 'grid' ? ( ) : ( diff --git a/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfig-4044.test.tsx b/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfig-4044.test.tsx index 8bb05031b3..2220d09c0a 100644 --- a/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfig-4044.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfig-4044.test.tsx @@ -71,6 +71,19 @@ import '@object-ui/components'; import '@object-ui/plugin-charts'; import '../index'; import { DashboardRenderer } from '../DashboardRenderer'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ /** Every composed chart node the relays handed the renderer, in order. */ const composed: any[] = []; @@ -95,7 +108,7 @@ const dataSource = { aggregate: async () => [], find: async () => [] }; const composeVia = async (surface: 'grid' | 'renderer', widget: Record) => { composed.length = 0; render( - + {surface === 'grid' ? ( ) : ( diff --git a/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfigDom-4044.test.tsx b/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfigDom-4044.test.tsx index f077752481..d55d5bbe0b 100644 --- a/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfigDom-4044.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfigDom-4044.test.tsx @@ -57,6 +57,19 @@ import '@object-ui/components'; import '@object-ui/plugin-charts'; import '../index'; import { DashboardRenderer } from '../DashboardRenderer'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ afterEach(cleanup); @@ -90,7 +103,7 @@ const renderWidget = async (surface: (typeof SURFACES)[number], chartConfig?: Re ...(chartConfig ? { chartConfig } : {}), }; const view = render( - + {surface === 'grid' ? ( ) : ( diff --git a/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfigMarks-4044.test.tsx b/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfigMarks-4044.test.tsx index 498f8a4e90..466dcb4ae0 100644 --- a/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfigMarks-4044.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/DashboardChart.chartConfigMarks-4044.test.tsx @@ -59,6 +59,19 @@ import '@object-ui/components'; import '@object-ui/plugin-charts'; import '../index'; import { DashboardRenderer } from '../DashboardRenderer'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ const PLOT_BOX = { width: 480, height: 320 } as const; const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect; @@ -116,7 +129,7 @@ const renderWidget = async ( ...(chartConfig ? { chartConfig } : {}), }; const view = render( - + {surface === 'grid' ? ( ) : ( diff --git a/packages/plugin-dashboard/src/__tests__/DashboardChart.countSeriesKey-8266.test.tsx b/packages/plugin-dashboard/src/__tests__/DashboardChart.countSeriesKey-8266.test.tsx index 5aa53469c8..3eb49ca1de 100644 --- a/packages/plugin-dashboard/src/__tests__/DashboardChart.countSeriesKey-8266.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/DashboardChart.countSeriesKey-8266.test.tsx @@ -58,6 +58,19 @@ import '@object-ui/components'; import '@object-ui/plugin-charts'; import '../index'; import { DashboardRenderer } from '../DashboardRenderer'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ /** Every composed chart node the relays handed the renderer, in order. */ const composed: any[] = []; @@ -82,7 +95,7 @@ const dataSource = { aggregate: async () => [], find: async () => [] }; const composeVia = async (surface: 'grid' | 'renderer', widget: Record) => { composed.length = 0; render( - + {surface === 'grid' ? ( ) : ( diff --git a/packages/plugin-dashboard/src/__tests__/DatasetWidget.relabel.test.tsx b/packages/plugin-dashboard/src/__tests__/DatasetWidget.relabel.test.tsx index 9e73bc589e..50dd7a85cc 100644 --- a/packages/plugin-dashboard/src/__tests__/DatasetWidget.relabel.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/DatasetWidget.relabel.test.tsx @@ -18,6 +18,19 @@ import { render, cleanup, waitFor } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; import { SchemaRendererProvider, type ApiFetch } from '@object-ui/react'; import { DatasetWidget } from '../DatasetWidget'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ let capturedChartSchema: any = null; beforeAll(() => { @@ -191,7 +204,7 @@ describe('DatasetWidget option-color / dimension-label probe routing (objectui#4 const host = makeMetaFetchRecorder(); render( - + , ); @@ -232,7 +245,7 @@ describe('DatasetWidget option-color / dimension-label probe routing (objectui#4 // provider is not presence of a channel, so this must stay on the fallback // rather than resolve to `undefined` and skip the read. render( - + , ); diff --git a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.bindNotForwarded-6575.test.tsx b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.bindNotForwarded-6575.test.tsx index 02b89783ae..470d3e61b4 100644 --- a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.bindNotForwarded-6575.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.bindNotForwarded-6575.test.tsx @@ -76,6 +76,19 @@ vi.mock('@object-ui/react', async () => { }); import { ObjectDataTable } from '../ObjectDataTable'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ afterEach(() => { cleanup(); @@ -101,7 +114,7 @@ const BOUND_SCHEMA = { function renderBound() { return render( - + , diff --git a/packages/plugin-detail/src/useRecordEditable.test.tsx b/packages/plugin-detail/src/useRecordEditable.test.tsx index 1ab2ac024a..efb3877d66 100644 --- a/packages/plugin-detail/src/useRecordEditable.test.tsx +++ b/packages/plugin-detail/src/useRecordEditable.test.tsx @@ -24,6 +24,18 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; import { SchemaRendererProvider } from '@object-ui/react'; import { useRecordEditable, __clearRecordEditableCache } from './useRecordEditable'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are placeholders for a provider that merely has to EXIST, and one probe + * that pins the empty-object case by name. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ function mockExplain(body: unknown, ok = true) { return vi.fn(async () => ({ ok, json: async () => body })) as any; @@ -105,7 +117,7 @@ describe('useRecordEditable', () => { vi.stubGlobal('fetch', globalFetch); const wrapper = ({ children }: { children: React.ReactNode }) => ( - + {children} ); diff --git a/packages/plugin-gantt/src/index.tsx b/packages/plugin-gantt/src/index.tsx index b4822f69af..cf28d634e1 100644 --- a/packages/plugin-gantt/src/index.tsx +++ b/packages/plugin-gantt/src/index.tsx @@ -88,7 +88,13 @@ const OBJECT_GANTT_DATA_SOURCE: ElementDataSourceMapping = { // Register component export const ObjectGanttRenderer: React.FC<{ schema: any }> = elementDataSourceBlock(({ schema }) => { - const { dataSource } = useSchemaContext() || {}; + // `useSchemaContext()` may hand back a NULL adapter: a host with nothing + // bound spells absence either way, and the seam declares both + // (`DataSource | null | undefined`, objectui#7912). The widget below + // declares the single spelling `dataSource?: DataSource`, so collapse the + // two absences into that one here rather than widening the widget. + const { dataSource: contextDataSource } = useSchemaContext() || {}; + const dataSource = contextDataSource ?? undefined; // The spec's `PageComponentSchema.dataSource` binding (objectstack#7121). A // gantt authored with the binding and no flat `objectName` produced no data // config at all, so `resolveDataSource` had nothing to fetch through: an empty diff --git a/packages/plugin-kanban/src/index.tsx b/packages/plugin-kanban/src/index.tsx index 584199f08b..da52ecb3cc 100644 --- a/packages/plugin-kanban/src/index.tsx +++ b/packages/plugin-kanban/src/index.tsx @@ -412,7 +412,13 @@ const OBJECT_KANBAN_DATA_SOURCE: ElementDataSourceMapping = { // Register object-kanban for ListView integration export const ObjectKanbanRenderer: React.FC<{ schema: any; [key: string]: any }> = elementDataSourceBlock(({ schema, ...props }) => { - const { dataSource } = useSchemaContext() || {}; + // `useSchemaContext()` may hand back a NULL adapter: a host with nothing + // bound spells absence either way, and the seam declares both + // (`DataSource | null | undefined`, objectui#7912). The widget below + // declares the single spelling `dataSource?: DataSource`, so collapse the + // two absences into that one here rather than widening the widget. + const { dataSource: contextDataSource } = useSchemaContext() || {}; + const dataSource = contextDataSource ?? undefined; // The spec's `PageComponentSchema.dataSource` binding (objectstack#6953): // before this, a board authored with `dataSource: { object, view }` and no // `objectName` never fetched — the effect is gated on `schema.objectName` — diff --git a/packages/plugin-kanban/src/registration.test.tsx b/packages/plugin-kanban/src/registration.test.tsx index a99f7b0f9a..cac58ee2b8 100644 --- a/packages/plugin-kanban/src/registration.test.tsx +++ b/packages/plugin-kanban/src/registration.test.tsx @@ -2,6 +2,7 @@ import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import React from 'react'; import { ObjectKanbanRenderer } from './index'; +import type { DataSource } from '@object-ui/types'; // Partial mock — override ONLY what this test controls, keep every other real // export. Same conversion `plugin-calendar/src/registration.test.tsx` already @@ -14,7 +15,11 @@ import { ObjectKanbanRenderer } from './index'; vi.mock(import('@object-ui/react'), async (importOriginal) => ({ ...(await importOriginal()), // Only the piece this test drives: - useSchemaContext: vi.fn(() => ({ dataSource: { type: 'mock-datasource' } })), + // The marker object below is NOT an adapter: the stubbed widget prints + // `dataSource.type`, which is the whole point of this registration probe. + // `useSchemaContext` declares the published `DataSource` contract since + // objectui#7912, so the crossing is explicit; the value is unchanged. + useSchemaContext: vi.fn(() => ({ dataSource: { type: 'mock-datasource' } as unknown as DataSource })), })); // Mock the implementation diff --git a/packages/plugin-list/src/ObjectGallery.tsx b/packages/plugin-list/src/ObjectGallery.tsx index c4ccd6a96c..e319b13bad 100644 --- a/packages/plugin-list/src/ObjectGallery.tsx +++ b/packages/plugin-list/src/ObjectGallery.tsx @@ -11,7 +11,7 @@ import { useDataScope, SchemaRendererContext, useNavigationOverlay, useSafeField import { ComponentRegistry, buildExpandFields, getRecordDisplayName, isEmptyValue } from '@object-ui/core'; import { cn, Card, CardContent, NavigationOverlay } from '@object-ui/components'; import { usePermissions } from '@object-ui/permissions'; -import type { GalleryConfig, ObjectGallerySchema } from '@object-ui/types'; +import type { DataSource, GalleryConfig, ObjectGallerySchema, QueryParams } from '@object-ui/types'; import { ChevronRight, ChevronDown } from 'lucide-react'; import { getCellRenderer, resolveCellRendererType, readFileValues } from '@object-ui/fields'; @@ -23,7 +23,15 @@ export interface ObjectGalleryProps { */ schema: ObjectGallerySchema; data?: Record[]; - dataSource?: { find: (name: string, query: unknown) => Promise }; + /** + * The host's adapter. Declared as the published `DataSource` contract + * (objectui#7912) — this used to be a hand-rolled `{ find(name, query: + * unknown): Promise }` stand-in, which is a second, weaker + * spelling of a type this repo already publishes: it accepted any object + * with a `find`, and it erased `find`'s real parameter and return types at + * every call below. + */ + dataSource?: DataSource; onCardClick?: (record: Record) => void; /** Callback when a row/item is clicked (overrides NavigationConfig) */ onRowClick?: (record: Record) => void; @@ -291,7 +299,10 @@ export const ObjectGallery: React.FC = (props) => { */ const { ready: objectDefReady, def: objectDef } = useSettledSchema( schema.objectName ?? '', - dataSource as any, + // No cast: `useSettledSchema` declares `DataSource | null | + // undefined` and, since objectui#7912, that is exactly what the seam + // hands over. + dataSource, ); // Permissions context, read here rather than inside the fetch effect below: @@ -428,15 +439,35 @@ export const ObjectGallery: React.FC = (props) => { ? expandable : expandable.filter((f) => perms.checkField(schema.objectName as string, f, 'read')); const results = await dataSource.find(schema.objectName, { - $filter: schema.filter, + // `ObjectGallerySchema.filter` is declared `unknown` — the + // one view schema in `@object-ui/types` whose `filter` is + // not `any[]` — and its docblock says it is "forwarded + // verbatim as `$filter`". Typing the adapter above makes + // `find`'s parameter real, so the verbatim forward has to + // name the parameter's own type instead of riding on + // `unknown`. Asserted, not coerced: the value is passed + // through byte-for-byte, exactly as before. + $filter: schema.filter as QueryParams['$filter'], ...(expand.length > 0 ? { $expand: expand } : {}), }); + // `find` now DECLARES `QueryResult`, whose only required + // member is `data` (objectui#7912 typed the adapter). This + // block predates that declaration and sniffs three envelopes: + // a bare array, `{ records }`, and the declared `{ data }`. + // + // Every branch is kept and every runtime path is unchanged. The + // declared value is widened ONCE, here, so the existing checks + // keep doing their own narrowing instead of being deleted on + // the strength of a declaration: whether any adapter really + // answers with the two UNDECLARED envelopes is a question about + // the adapters, and answering it is not this card's business. + const envelope: unknown = results; let data: Record[] = []; - if (Array.isArray(results)) { - data = results; - } else if (results && typeof results === 'object') { - const r = results as Record; + if (Array.isArray(envelope)) { + data = envelope; + } else if (envelope && typeof envelope === 'object') { + const r = envelope as Record; if (Array.isArray(r.records)) { data = r.records as Record[]; } else if (Array.isArray(r.data)) { diff --git a/packages/plugin-list/src/__tests__/ListView.ariaLabelInlineLocale.test.tsx b/packages/plugin-list/src/__tests__/ListView.ariaLabelInlineLocale.test.tsx index b46f7f66e2..3eeba87f01 100644 --- a/packages/plugin-list/src/__tests__/ListView.ariaLabelInlineLocale.test.tsx +++ b/packages/plugin-list/src/__tests__/ListView.ariaLabelInlineLocale.test.tsx @@ -60,9 +60,21 @@ import { render, screen, cleanup } from '@testing-library/react'; import '@testing-library/jest-dom'; import { LocalizationProvider } from '@object-ui/i18n'; import { SchemaRendererProvider } from '@object-ui/react'; -import type { ListViewSchema } from '@object-ui/types'; +import type { DataSource, ListViewSchema } from '@object-ui/types'; import { ListView } from '../ListView'; +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ + const mockDataSource = { find: vi.fn().mockResolvedValue([]), findOne: vi.fn(), @@ -85,7 +97,7 @@ function renderListView(aria: unknown, locale: string) { return render( - + , diff --git a/packages/plugin-list/src/__tests__/ListView.density.offspec.test.tsx b/packages/plugin-list/src/__tests__/ListView.density.offspec.test.tsx index 6e7975e4f0..fd5921f2a5 100644 --- a/packages/plugin-list/src/__tests__/ListView.density.offspec.test.tsx +++ b/packages/plugin-list/src/__tests__/ListView.density.offspec.test.tsx @@ -9,9 +9,21 @@ import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import { SchemaRendererProvider } from '@object-ui/react'; -import type { ListViewSchema } from '@object-ui/types'; +import type { DataSource, ListViewSchema } from '@object-ui/types'; import { ListView } from '../ListView'; +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ + /** * What an off-spec `rowHeight` actually RENDERS — the empirical half of * objectui#4440, pinned on the live path rather than argued from the mapping. @@ -53,7 +65,7 @@ const renderListView = (rowHeight?: unknown) => { } as ListViewSchema; return render( - + , ); diff --git a/packages/plugin-list/src/__tests__/ListView.descriptionInlineLocale-7199.test.tsx b/packages/plugin-list/src/__tests__/ListView.descriptionInlineLocale-7199.test.tsx index 724cce7a75..3f8b459f53 100644 --- a/packages/plugin-list/src/__tests__/ListView.descriptionInlineLocale-7199.test.tsx +++ b/packages/plugin-list/src/__tests__/ListView.descriptionInlineLocale-7199.test.tsx @@ -75,9 +75,21 @@ import { render, screen, cleanup } from '@testing-library/react'; import '@testing-library/jest-dom'; import { LocalizationProvider } from '@object-ui/i18n'; import { SchemaRendererProvider } from '@object-ui/react'; -import type { ListViewSchema } from '@object-ui/types'; +import type { DataSource, ListViewSchema } from '@object-ui/types'; import { ListView } from '../ListView'; +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ + const mockDataSource = { find: vi.fn().mockResolvedValue([]), findOne: vi.fn(), @@ -111,7 +123,7 @@ function renderDescription( return render( - + , diff --git a/packages/plugin-list/src/__tests__/ListView.overlayTitleI18n.test.tsx b/packages/plugin-list/src/__tests__/ListView.overlayTitleI18n.test.tsx index 7f0d02fbcd..09b45357fb 100644 --- a/packages/plugin-list/src/__tests__/ListView.overlayTitleI18n.test.tsx +++ b/packages/plugin-list/src/__tests__/ListView.overlayTitleI18n.test.tsx @@ -51,7 +51,19 @@ import { ComponentRegistry } from '@object-ui/core'; import { I18nProvider } from '@object-ui/i18n'; import { SchemaRendererProvider } from '@object-ui/react'; import { ListView } from '../ListView'; -import type { ListViewSchema } from '@object-ui/types'; +import type { DataSource, ListViewSchema } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ const rows = [{ id: '1', name: 'Alice' }]; @@ -89,7 +101,7 @@ afterEach(() => cleanup()); function renderListIn(language: string, schemaExtra: Partial) { return render( - + { it('reuses detail.recordDetail when the schema names nothing', async () => { render( - + cleanup()); function renderList(schemaExtra: Record) { return render( - + { let store: Record = {}; @@ -34,7 +46,7 @@ const mockDataSource = { const renderWithProvider = (component: React.ReactNode) => { return render( - + {component} ); diff --git a/packages/plugin-list/src/__tests__/UserFilters.addTab.test.tsx b/packages/plugin-list/src/__tests__/UserFilters.addTab.test.tsx index 44386aede8..02dc6ff4e1 100644 --- a/packages/plugin-list/src/__tests__/UserFilters.addTab.test.tsx +++ b/packages/plugin-list/src/__tests__/UserFilters.addTab.test.tsx @@ -30,6 +30,19 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, fireEvent, cleanup } from '@testing-library/react'; import { SchemaRendererProvider } from '@object-ui/react'; import { UserFilters } from '../UserFilters'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are partial stubs carrying only the members the path under test calls; + * completing them would change which capability probes fire, and so would + * change what these tests measure. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ afterEach(() => { cleanup(); @@ -211,7 +224,7 @@ describe('UserFilters tabs — adding a tab writes nothing (ADR-0047)', () => { setItem.mockClear(); render( - + ``` +`dataSource` carries the host's adapter as the published `DataSource` contract +(`@object-ui/types`), typed `DataSource | null | undefined`: a host either hands +over an adapter or states that it has none — a Studio preview, a page rendered +before the host's adapter has connected, or a widget probe driving `apiFetch` +alone. Both spellings of "none" are accepted because every reader in the tree +guards for it. The prop used to be typed `any`, so a string passed where the +adapter belongs — the shape of a config value read from the wrong place — +raised nothing until the first `find()` at runtime (objectui#7912). +`useSchemaContext()` hands the same type back to every consumer. + Nested providers inherit `apiFetch` from their parent when they don't supply their own, so re-wrapped subtrees (embedded pages, preview surfaces) keep the host's authentication. diff --git a/packages/react/src/__tests__/SchemaRenderer.debug.test.tsx b/packages/react/src/__tests__/SchemaRenderer.debug.test.tsx index f9e0af8191..f05938c314 100644 --- a/packages/react/src/__tests__/SchemaRenderer.debug.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.debug.test.tsx @@ -12,6 +12,18 @@ import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; import { SchemaRenderer } from '../SchemaRenderer'; import { SchemaRendererProvider } from '../context/SchemaRendererContext'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are placeholders for a provider that merely has to EXIST, and one probe + * that pins the empty-object case by name. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ // Suppress console.warn from deprecated namespace registration const originalWarn = console.warn; @@ -35,7 +47,7 @@ describe('SchemaRenderer debug attributes', () => { it('should NOT inject data-debug-* attributes when debug is off', () => { const { getByTestId } = render( - + , ); @@ -46,7 +58,7 @@ describe('SchemaRenderer debug attributes', () => { it('should inject data-debug-type when debug is enabled', () => { const { getByTestId } = render( - + , ); @@ -57,7 +69,7 @@ describe('SchemaRenderer debug attributes', () => { it('should inject data-debug-type when debugFlags.enabled is true', () => { const { getByTestId } = render( - + , ); diff --git a/packages/react/src/__tests__/SchemaRenderer.disabledDeclaredGate.test.tsx b/packages/react/src/__tests__/SchemaRenderer.disabledDeclaredGate.test.tsx index 68b6ab1b6a..ee70f86461 100644 --- a/packages/react/src/__tests__/SchemaRenderer.disabledDeclaredGate.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.disabledDeclaredGate.test.tsx @@ -81,6 +81,19 @@ import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; import { SchemaRenderer } from '../SchemaRenderer'; import { SchemaRendererContext } from '../context/SchemaRendererContext'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ /** * Records the `disabled` prop EXACTLY as it arrives — `absent` when the renderer @@ -97,7 +110,7 @@ const DATA = { status: 'locked', readOnly: true, unlocked: false }; function renderNode(schema: Record) { return render( - + , ); diff --git a/packages/react/src/__tests__/SchemaRenderer.expressions.test.tsx b/packages/react/src/__tests__/SchemaRenderer.expressions.test.tsx index 0f45d0b1db..a638a818da 100644 --- a/packages/react/src/__tests__/SchemaRenderer.expressions.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.expressions.test.tsx @@ -23,6 +23,19 @@ import { SchemaRenderer } from '../SchemaRenderer'; // the same reason), and the two casts are gone. The `BaseSchema` import went // with them — nothing in this file needs the name any more. import { SchemaRendererContext } from '../context/SchemaRendererContext'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ // Simple test component const TestComponent = (props: any) => ( @@ -53,7 +66,7 @@ describe('SchemaRenderer Expression Integration', () => { it('evaluates visible expression string', () => { render( - + ); @@ -62,7 +75,7 @@ describe('SchemaRenderer Expression Integration', () => { it('hides when visible expression evaluates to false', () => { const { container } = render( - + ); @@ -81,7 +94,7 @@ describe('SchemaRenderer Expression Integration', () => { it('hides with hiddenOn expression', () => { const { container } = render( - + ); @@ -99,7 +112,7 @@ describe('SchemaRenderer Expression Integration', () => { describe('visibleWhen (ADR-0089 canonical)', () => { it('shows when the visibleWhen predicate is truthy', () => { render( - + ); @@ -108,7 +121,7 @@ describe('SchemaRenderer Expression Integration', () => { it('hides when the visibleWhen predicate is falsy', () => { const { container } = render( - + ); @@ -117,7 +130,7 @@ describe('SchemaRenderer Expression Integration', () => { it('still honors the deprecated `visibility` alias', () => { const { container } = render( - + ); @@ -133,7 +146,7 @@ describe('SchemaRenderer Expression Integration', () => { it('evaluates disabled expression string', () => { render( - + ); @@ -142,7 +155,7 @@ describe('SchemaRenderer Expression Integration', () => { it('does not set disabled when expression is false', () => { render( - + ); @@ -151,7 +164,7 @@ describe('SchemaRenderer Expression Integration', () => { it('evaluates disabledOn expression', () => { render( - + ); diff --git a/packages/react/src/__tests__/SchemaRenderer.hiddenDeclaredGate.test.tsx b/packages/react/src/__tests__/SchemaRenderer.hiddenDeclaredGate.test.tsx index d53b7a123b..630b348974 100644 --- a/packages/react/src/__tests__/SchemaRenderer.hiddenDeclaredGate.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.hiddenDeclaredGate.test.tsx @@ -89,10 +89,22 @@ import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; -import type { BaseSchema } from '@object-ui/types'; +import type { BaseSchema, DataSource } from '@object-ui/types'; import { SchemaRenderer } from '../SchemaRenderer'; import { SchemaRendererContext } from '../context/SchemaRendererContext'; +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ + /** * Records the `hidden` prop exactly as it arrives, so "the node rendered" and * "the schema key leaked into the DOM props" are separate observations. @@ -108,7 +120,7 @@ const DATA = { status: 'draft', archived: true, published: false }; function renderNode(schema: Record) { return render( - + , ); @@ -139,7 +151,7 @@ function renderNode(schema: Record) { */ function renderDeclaredNode(schema: BaseSchema) { return render( - + , ); diff --git a/packages/react/src/__tests__/SchemaRenderer.predicateEnvelopeConfigBag.test.tsx b/packages/react/src/__tests__/SchemaRenderer.predicateEnvelopeConfigBag.test.tsx index 58896ba1f1..8fe4e76ba0 100644 --- a/packages/react/src/__tests__/SchemaRenderer.predicateEnvelopeConfigBag.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.predicateEnvelopeConfigBag.test.tsx @@ -56,6 +56,19 @@ import React from 'react'; import { ComponentRegistry, ExpressionEvaluator } from '@object-ui/core'; import { SchemaRenderer } from '../SchemaRenderer'; import { SchemaRendererContext } from '../context/SchemaRendererContext'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ const Probe = (props: { schema?: { props?: Record } }) => (
+ , ); diff --git a/packages/react/src/__tests__/SchemaRenderer.predicateEnvelopeDeclared.test.tsx b/packages/react/src/__tests__/SchemaRenderer.predicateEnvelopeDeclared.test.tsx index 487e8628c4..fa67d9dec6 100644 --- a/packages/react/src/__tests__/SchemaRenderer.predicateEnvelopeDeclared.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.predicateEnvelopeDeclared.test.tsx @@ -62,10 +62,22 @@ import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; -import type { BaseSchema, ExpressionWire } from '@object-ui/types'; +import type { BaseSchema, DataSource, ExpressionWire } from '@object-ui/types'; import { SchemaRenderer } from '../SchemaRenderer'; import { SchemaRendererContext } from '../context/SchemaRendererContext'; +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are the `data` ROOT of the expression scope — the renderer binds + * `SchemaRendererContext.dataSource` as `data` for every predicate, which is + * the second meaning this one key carries. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ + /** * Records the `disabled` prop exactly as it arrives, so "the node rendered" and * "the renderer forwarded `disabled`" are separate observations. @@ -82,7 +94,7 @@ const DATA = { status: 'draft', published: false }; /** The DECLARED path -- `BaseSchema`, nothing wider, no cast. */ function mount(schema: BaseSchema) { return render( - + , ); diff --git a/packages/react/src/__tests__/SchemaRenderer.validation.test.tsx b/packages/react/src/__tests__/SchemaRenderer.validation.test.tsx index 3f28bd9391..49bef1f89b 100644 --- a/packages/react/src/__tests__/SchemaRenderer.validation.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.validation.test.tsx @@ -12,6 +12,18 @@ import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; import { SchemaRenderer } from '../SchemaRenderer'; import { SchemaRendererProvider } from '../context/SchemaRendererContext'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are placeholders for a provider that merely has to EXIST, and one probe + * that pins the empty-object case by name. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ const PassthroughDiv: React.FC = (props) => { const { schema, ...rest } = props; @@ -37,7 +49,7 @@ describe('SchemaRenderer — dev-mode validation', () => { it('does not warn for a well-formed schema', () => { render( - + ); @@ -58,7 +70,7 @@ describe('SchemaRenderer — dev-mode validation', () => { ComponentRegistry.register('host-wrap', HostWithChild); render( - + { children: [bad], }; const { getByTestId } = render( - + ); @@ -106,7 +118,7 @@ describe('SchemaRenderer — dev-mode validation', () => { }; const { rerender } = render( - + ); @@ -115,7 +127,7 @@ describe('SchemaRenderer — dev-mode validation', () => { ).length; rerender( - + ); diff --git a/packages/react/src/__tests__/SchemaRendererProvider.smoke.test.tsx b/packages/react/src/__tests__/SchemaRendererProvider.smoke.test.tsx index 24751616f1..f9f16a17e3 100644 --- a/packages/react/src/__tests__/SchemaRendererProvider.smoke.test.tsx +++ b/packages/react/src/__tests__/SchemaRendererProvider.smoke.test.tsx @@ -21,6 +21,18 @@ import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; import { SchemaRenderer } from '../SchemaRenderer'; import { SchemaRendererProvider, useSchemaContext } from '../context/SchemaRendererContext'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * they are placeholders for a provider that merely has to EXIST, and one probe + * that pins the empty-object case by name. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ // Suppress console.error from React error boundary during tests const originalConsoleError = console.error; @@ -46,7 +58,7 @@ describe('useSchemaContext provider requirement', () => { it('should not throw when used inside SchemaRendererProvider', () => { render( - + ); @@ -55,7 +67,7 @@ describe('useSchemaContext provider requirement', () => { it('should fall back to empty dataSource when provider has empty object', () => { render( - + ); @@ -77,8 +89,8 @@ describe('SchemaRendererProvider apiFetch inheritance (#2725)', () => { it('nested provider without apiFetch inherits the parent host fetch', () => { render( - - + + @@ -88,8 +100,8 @@ describe('SchemaRendererProvider apiFetch inheritance (#2725)', () => { it('nested provider with its own apiFetch overrides the parent', () => { render( - - + + @@ -99,7 +111,7 @@ describe('SchemaRendererProvider apiFetch inheritance (#2725)', () => { it('apiFetch stays undefined when no provider supplies one', () => { render( - + ); @@ -118,7 +130,7 @@ describe('SchemaRenderer + SchemaRendererProvider integration', () => { it('should render a component that calls useSchemaContext without error when provider wraps the tree', () => { render( - + ); @@ -160,7 +172,7 @@ describe('Plugin component types render inside provider', () => { // Render via SchemaRenderer inside provider const { container } = render( - + ); diff --git a/packages/react/src/context/SchemaRendererContext.tsx b/packages/react/src/context/SchemaRendererContext.tsx index 55efccde74..9c2e3357f7 100644 --- a/packages/react/src/context/SchemaRendererContext.tsx +++ b/packages/react/src/context/SchemaRendererContext.tsx @@ -1,5 +1,6 @@ import React, { createContext, useContext, useMemo } from 'react'; import type { DebugFlags } from '@object-ui/core'; +import type { DataSource } from '@object-ui/types'; /** * Host-provided fetch used for `provider: 'api'` view data sources so custom @@ -10,7 +11,29 @@ import type { DebugFlags } from '@object-ui/core'; export type ApiFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; interface SchemaRendererContextType { - dataSource: any; + /** + * The adapter the host injected, as the published `DataSource` contract + * declares it (objectui#7912). + * + * Typed, not `any`: this is the injection point for the ENTIRE renderer tree, + * and `useSchemaContext()` hands whatever is declared here to every consumer + * that reads it back. While it was `any`, a bare string passed where the + * host's adapter belongs raised nothing at compile time and failed at runtime + * on the first `find()` — the shape of a config value read from the wrong + * place. The type was never unknown: `@object-ui/types` exports `DataSource`, + * `useSettledSchema` in this same package already declares its parameter as + * `DataSource | null | undefined`, and `app-shell`'s README writes + * `DataSource` against this very seam. + * + * `null | undefined` are part of the contract, not a weakening of it: a host + * renders with no adapter bound (a Studio preview, a react page before the + * AdapterProvider connects, a widget test that drives `apiFetch` alone), and + * every reader in the tree already guards for it. The union states the three + * states that actually occur and refuses everything else — a string, `{}`, a + * plain data bag, a partial adapter missing a required member. It is spelled + * exactly as `useSettledSchema` spells it, one hop away. + */ + dataSource: DataSource | null | undefined; debug?: boolean; debugFlags?: DebugFlags; apiFetch?: ApiFetch; @@ -28,7 +51,8 @@ export const SchemaRendererProvider = ({ apiFetch, }: { children: React.ReactNode; - dataSource: any; + /** The host's adapter — see {@link SchemaRendererContextType.dataSource}. */ + dataSource: DataSource | null | undefined; debug?: boolean; debugFlags?: DebugFlags; apiFetch?: ApiFetch; @@ -62,6 +86,21 @@ export const useDataScope = (path?: string) => { const dataSource = context?.dataSource; if (!path) return undefined; if (!dataSource) return undefined; - // Simple path resolution for now. In real app might be more complex - return path.split('.').reduce((acc, part) => acc && acc[part], dataSource); + // Simple path resolution for now. In real app might be more complex. + // + // The accumulator is `any` BY DECLARATION, not by inheritance: this walk + // addresses arbitrary member names on the injected value, and `DataSource` + // declares none of them, so indexing it by a path segment is an error the + // moment the seam above stops being `any` (objectui#7912). The hook's + // published return type is unchanged — it was `any` before this annotation + // and it is `any` after it, so no reader of `useDataScope` moves. + // + // ⚠️ What the type now makes visible: against a REAL adapter every path + // resolves to `undefined`, because an adapter has no `users`/`value` member + // to walk. Hosts that get data out of this hook are injecting a data bag + // through a key the contract says is an adapter. Whether that second meaning + // becomes real or is retired is NOT decided here — same shape, and the same + // deliberate non-decision, as the `ctx?.formValues ?? ctx?.data` tail on + // objectui#7206. + return path.split('.').reduce((acc, part) => acc && acc[part], dataSource); } diff --git a/packages/react/src/context/__tests__/SchemaRendererContext.dataSourceType.pin.test.ts b/packages/react/src/context/__tests__/SchemaRendererContext.dataSourceType.pin.test.ts new file mode 100644 index 0000000000..3d2a413df5 --- /dev/null +++ b/packages/react/src/context/__tests__/SchemaRendererContext.dataSourceType.pin.test.ts @@ -0,0 +1,260 @@ +/** + * 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. + */ + +/** + * `SchemaRendererProvider`'s `dataSource` prop AND the context type the whole + * tree reads back are the published `DataSource` contract, and the compiler + * knows it (objectui#7912). + * + * ## What went wrong, and why only the compiler can see it + * + * Both members were declared `any`: the provider's own prop, and + * `SchemaRendererContextType.dataSource`, which `useSchemaContext()` hands to + * every consumer in the tree. So a bare string passed where the host's adapter + * belongs raised nothing — and that is not a hypothetical mistake, it is the + * shape of a config value read from the wrong place, failing at runtime on the + * first `find()`. The type was never unknown: `@object-ui/types` exports + * `DataSource`, `useSettledSchema` in this same package already declared its + * parameter as `DataSource | null | undefined`, and `app-shell`'s README + * writes `DataSource` against this very seam. + * + * A runtime test cannot observe any of this. Which assignments the compiler + * refuses is erased before an assertion could run, and while the seam was + * `any` every wrong value succeeded at runtime until the first adapter call. + * So the gauge has to be `tsc`, driven here — the same harness as + * `RecordContext.dataSourceType.pin.test.ts` (objectui#9197), for the same + * reason. + * + * ## Pinned in BOTH directions, through the PUBLISHED surface + * + * The rows do not name the internal interface. They go through + * `Parameters[0]` and `ReturnType`, because "no consumer can be told anything about it" is + * the defect, and those two are what a consumer can reach. + * + * - ACCEPTED: a host may hand over an adapter with no cast; a host with nothing + * bound may say so (`undefined` / `null` — the Studio preview, a react page + * before its AdapterProvider connects, a widget test driving `apiFetch` + * alone); a reader may reach `find` off the context after guarding. + * - REFUSED: the card's own planted probe (`'not-an-adapter'`), the empty + * object that used to be passed as a "no adapter" stand-in, a partial adapter + * missing a REQUIRED member, and a plain data bag. These rows are the + * acceptance of objectui#7912: each one is silently accepted by `any`, so + * reverting either declaration turns them green and this file red. + * + * ## Resolution guard (why the CONTROL rows exist) + * + * The harness resolves the context from its SOURCE path and `@object-ui/types` + * through the repo's `paths`, not through `dist`. A type-level pin's + * characteristic failure is the harness degrading everything to `any`: every + * REFUSED row would flip to accepted while the file still looks perfect. The + * CONTROL rows are refusals that have nothing to do with `dataSource` — a + * misspelled prop key, a missing required prop, and `DataSource`'s required + * members being visible at all — so if resolution ever stops being real, they + * fire. Any diagnostic landing in the virtual module's IMPORT HEADER throws + * loudly instead of being read as a verdict. + * + * Cost note (AGENTS.md 测试纪律): the program is built at MODULE SCOPE, so the + * compiler work lands in the import phase, which no test or hook timeout + * bounds. A `beforeAll` would put it under the narrower 10s `hookTimeout`. + */ + +import { describe, it, expect } from 'vitest'; +import ts from 'typescript'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from 'node:path'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +// __tests__ → context → src → react → packages → repo root +const REPO_ROOT = resolve(HERE, '..', '..', '..', '..', '..'); +/** The seam under test, by SOURCE path — no `dist`, no barrel. */ +const CONTEXT_IMPORT = join(HERE, '..', 'SchemaRendererContext').replace(/\\/g, '/'); + +/** + * One assignment or read, and whether `tsc` must refuse it. + * `code` emits EXACTLY one line — the line index is how a diagnostic is + * attributed back to a case. + */ +interface Case { + readonly what: string; + readonly code: string; + readonly refused: boolean; +} + +/** The context value as a CONSUMER reaches it. */ +const CTX = `(undefined as unknown as ReturnType)`; +/** The provider's props as a CONSUMER reaches them. */ +const PROPS = `Parameters[0]`; +/** A complete adapter, for rows that are not about completeness. */ +const ADAPTER = `(undefined as unknown as DataSource)`; + +const CASES: readonly Case[] = [ + // ── The acceptance of objectui#7912. These rows ARE the card. ───────────── + { + what: 'a host may hand the provider a DataSource adapter with no cast', + code: `const c: ${PROPS} = { children: null, dataSource: ${ADAPTER} };`, + refused: false, + }, + { + what: 'a host with no adapter bound may say so with `undefined`', + code: `const c: ${PROPS} = { children: null, dataSource: undefined };`, + refused: false, + }, + { + what: 'a host with no adapter bound may say so with `null`', + code: `const c: ${PROPS} = { children: null, dataSource: null };`, + refused: false, + }, + { + what: 'a reader reaches `find` off the context value with no cast, once guarded', + code: `const c: DataSource['find'] = ${CTX}.dataSource!.find;`, + refused: false, + }, + { + what: 'the context member reads as the adapter contract, absences included', + code: `const c: DataSource | null | undefined = ${CTX}.dataSource;`, + refused: false, + }, + { + what: 'the seam is one type on both sides — the prop is assignable to the context member', + code: `const c: ReturnType['dataSource'] = (undefined as unknown as ${PROPS})['dataSource'];`, + refused: false, + }, + + // ── The breaking half. Each of these is what `any` used to accept. ──────── + { + what: "BREAKING the card's planted probe — a bare string where the adapter belongs", + code: `const c: ${PROPS} = { children: null, dataSource: 'not-an-adapter' };`, + refused: true, + }, + { + what: 'BREAKING the context member no longer reads as a string', + code: `const c: string | null | undefined = ${CTX}.dataSource;`, + refused: true, + }, + { + what: 'BREAKING an empty object is not a "no adapter" stand-in any more', + code: `const c: ${PROPS} = { children: null, dataSource: {} };`, + refused: true, + }, + { + what: 'BREAKING a partial adapter missing a REQUIRED member is refused', + code: `const c: ${PROPS} = { children: null, dataSource: { find: (undefined as any), findOne: (undefined as any), create: (undefined as any), update: (undefined as any), delete: (undefined as any) } };`, + refused: true, + }, + { + what: 'BREAKING a plain data bag is refused where the adapter belongs', + code: `const c: ${PROPS} = { children: null, dataSource: { users: [] } };`, + refused: true, + }, + + // ── Controls. Nothing to do with `dataSource`; they prove the program is + // resolving the real types rather than degrading everything to `any`. ── + { + what: 'CONTROL a misspelled provider prop key is refused', + code: `const c: ${PROPS} = { children: null, dataSource: ${ADAPTER}, dataSorce: ${ADAPTER} };`, + refused: true, + }, + { + what: 'CONTROL a provider value missing the required `children` is refused', + code: `const c: ${PROPS} = { dataSource: ${ADAPTER} };`, + refused: true, + }, + { + what: 'CONTROL `DataSource` really declares `getObjectSchema` as REQUIRED', + code: `const c: DataSource = { find: (undefined as any), findOne: (undefined as any), create: (undefined as any), update: (undefined as any), delete: (undefined as any) };`, + refused: true, + }, +]; + +const IMPORTS = [ + `import type { DataSource } from '@object-ui/types';`, + `import type { SchemaRendererProvider, useSchemaContext } from '${CONTEXT_IMPORT}';`, + // Keeps the imports "used", so a reader of the virtual file can see why they + // are here even when a case stops mentioning one of them. + `type _Used = [DataSource, typeof SchemaRendererProvider, typeof useSchemaContext];`, +].join('\n'); + +/** + * Compile every case and return the set of case indices that produced a + * diagnostic. + * + * `paths` mirrors the repo root `tsconfig.json`, so the workspace specifiers + * resolve to source exactly as the workspace itself resolves them. See the file + * header for why default (`dist`-backed) resolution is not acceptable here. + */ +function erroringCases(): Set { + const header = `${IMPORTS}\n`; + // Each case is wrapped in its own BLOCK so the `const c` declarations do not + // collide — a duplicate-identifier diagnostic would land on every line and + // read as "the compiler refuses everything", which is the one wrong answer + // this file must never produce. Still exactly one line per case. + const body = CASES.map((c) => `{ ${c.code} }`).join('\n'); + const source = `${header}${body}\n`; + const headerLines = header.split('\n').length - 1; + + const VIRTUAL = join(HERE, '__schemaRendererDataSourcePins.virtual.ts').replace(/\\/g, '/'); + const options: ts.CompilerOptions = { + strict: true, + skipLibCheck: true, + noEmit: true, + jsx: ts.JsxEmit.ReactJSX, + moduleResolution: ts.ModuleResolutionKind.Bundler, + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ESNext, + baseUrl: REPO_ROOT, + paths: { + '@object-ui/types': ['packages/types/src'], + '@object-ui/types/*': ['packages/types/src/*'], + '@object-ui/core': ['packages/core/src'], + '@object-ui/core/*': ['packages/core/src/*'], + }, + }; + const host = ts.createCompilerHost(options); + const getSourceFile = host.getSourceFile.bind(host); + host.getSourceFile = (fileName, languageVersion, ...rest) => + fileName === VIRTUAL + ? ts.createSourceFile(fileName, source, languageVersion, true) + : getSourceFile(fileName, languageVersion, ...rest); + const fileExists = host.fileExists.bind(host); + host.fileExists = (fileName) => (fileName === VIRTUAL ? true : fileExists(fileName)); + const readFile = host.readFile.bind(host); + host.readFile = (fileName) => (fileName === VIRTUAL ? source : readFile(fileName)); + + const program = ts.createProgram([VIRTUAL], options, host); + const sf = program.getSourceFile(VIRTUAL); + if (!sf) throw new Error('virtual source file was not added to the program'); + + const cases = new Set(); + for (const d of [...program.getSemanticDiagnostics(sf), ...program.getSyntacticDiagnostics(sf)]) { + if (d.start == null) continue; + const index = sf.getLineAndCharacterOfPosition(d.start).line - headerLines; + // A diagnostic ABOVE the first case line is a broken import, not a verdict. + // Fail loudly rather than let it read as "the compiler accepted everything". + if (index < 0) { + throw new Error( + 'the pin harness failed to resolve its own imports — this is a setup failure, not a ' + + `verdict about \`dataSource\`: ${ts.flattenDiagnosticMessageText(d.messageText, ' ')}`, + ); + } + cases.add(index); + } + return cases; +} + +// Module scope on purpose — see the file header. +const refusedByCompiler = erroringCases(); + +describe('`SchemaRendererProvider.dataSource` is the published DataSource contract (objectui#7912)', () => { + for (const [i, c] of CASES.entries()) { + it(c.what, () => { + expect({ case: c.what, refused: refusedByCompiler.has(i) }) + .toEqual({ case: c.what, refused: c.refused }); + }); + } +}); diff --git a/packages/react/src/context/__tests__/useDataScope.test.tsx b/packages/react/src/context/__tests__/useDataScope.test.tsx index deff53df44..acd92221a7 100644 --- a/packages/react/src/context/__tests__/useDataScope.test.tsx +++ b/packages/react/src/context/__tests__/useDataScope.test.tsx @@ -6,11 +6,23 @@ import { describe, it, expect } from 'vitest'; import { renderHook } from '@testing-library/react'; import React from 'react'; import { SchemaRendererProvider, useDataScope } from '../SchemaRendererContext'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * `useDataScope` walks the injected value BY PATH, so a probe for it injects a + * bag rather than an adapter. + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ describe('useDataScope', () => { it('returns undefined when no path is provided', () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( - + {children} ); @@ -22,7 +34,7 @@ describe('useDataScope', () => { it('returns undefined when path is empty string', () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( - + {children} ); @@ -34,7 +46,7 @@ describe('useDataScope', () => { it('returns scoped data when a valid path is given', () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( - + {children} ); @@ -46,7 +58,7 @@ describe('useDataScope', () => { it('resolves nested paths', () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( - + {children} ); @@ -58,7 +70,7 @@ describe('useDataScope', () => { it('returns undefined for non-existent path', () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( - + {children} ); @@ -78,7 +90,7 @@ describe('useDataScope', () => { // Simulate the real scenario: dataSource is a service adapter with methods const adapter = { find: () => {}, create: () => {}, update: () => {} }; const wrapper = ({ children }: { children: React.ReactNode }) => ( - + {children} ); diff --git a/packages/react/src/hooks/useClientNotifications.ts b/packages/react/src/hooks/useClientNotifications.ts index 4fe5a15075..1f090e62f8 100644 --- a/packages/react/src/hooks/useClientNotifications.ts +++ b/packages/react/src/hooks/useClientNotifications.ts @@ -31,6 +31,7 @@ import { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { SchemaRendererContext } from '../context/SchemaRendererContext.js'; import { useNotifications } from '../context/NotificationContext.js'; import type { NotificationSeverityLevel } from '../context/NotificationContext.js'; +import type { DataSource } from '@object-ui/types'; /* ------------------------------------------------------------------ */ /* Public types */ @@ -69,6 +70,23 @@ function toSeverity(value: unknown): NotificationSeverityLevel { return 'info'; } +/** + * An adapter that wraps an `@objectstack/client` and hands it out. + * + * `getClient` is NOT a `DataSource` member and this declaration does not make + * it one: it is an ObjectStackAdapter CAPABILITY that only some adapters have, + * so it is declared here, next to its only reader, and probed structurally. + * Declaring the capability is what lets the seam keep its real type — the + * alternative, casting the context value back to `any` to reach a member the + * contract does not promise, would re-create objectui#7912's defect one line + * below the seam it just fixed. + */ +type ClientBearingDataSource = DataSource & { getClient(): unknown }; + +function hasGetClient(dataSource: DataSource): dataSource is ClientBearingDataSource { + return typeof (dataSource as Partial).getClient === 'function'; +} + /** * Resolve the ObjectStack client from explicit option or SchemaRendererContext. * @@ -81,7 +99,7 @@ function useResolvedClient(explicit?: any): any { if (explicit) return explicit; const dataSource = rendererCtx?.dataSource; - if (dataSource && typeof dataSource.getClient === 'function') { + if (dataSource && hasGetClient(dataSource)) { return dataSource.getClient(); } return null; From 64c05fdbf5e9269756790bb6db4adb026dd08e06 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 17:27:20 +0000 Subject: [PATCH 2/3] fix(site): the live demo has no adapter to inject, and now says so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's "Build Docs" job caught the one injection site the type-check union in the previous commit did not cover: `apps/site` was excluded from that filter, and `LiveSplitDemo` handed the provider a truthy empty object as its "no adapter" stand-in — the same shape already corrected in the `kind:'react'` page. Every preset this component renders is a static schema; the object-bound examples live in `InteractiveDemo` and `SchemaThumbnail`, which inject the docs gallery adapter. So this surface has no adapter, and `undefined` is what that is. Still a module constant, because the memo below keys on its identity. The union is corrected with it: dropping `--filter='!@object-ui/site'` puts the site's own `type-check` back in scope, which is where this would have surfaced locally. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ --- apps/site/app/components/LiveSplitDemo.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/site/app/components/LiveSplitDemo.tsx b/apps/site/app/components/LiveSplitDemo.tsx index ac73a33d12..cfc49b1730 100644 --- a/apps/site/app/components/LiveSplitDemo.tsx +++ b/apps/site/app/components/LiveSplitDemo.tsx @@ -43,7 +43,19 @@ const PRESET_LABELS: Record<(typeof PRESET_IDS)[number], string> = { 'components-complex-table/basic-table': 'Table', }; -const defaultCtx = { dataSource: {} }; +/** + * The provider value these demos render under. Every preset in `PRESET_IDS` is + * a STATIC schema — the object-bound examples live in `InteractiveDemo` / + * `SchemaThumbnail`, which inject `galleryDataSource` — so this surface has no + * adapter to hand over and says so with `undefined` (objectui#7912). + * + * It used to say `{}`. An empty object is TRUTHY, so it walked past the + * `if (!dataSource)` guard every reader writes for exactly this state; the seam + * now declares `DataSource | null | undefined`, so the absence is stated rather + * than smuggled. Still a module constant, for the same reason as before: the + * memo below keys on its identity, and `undefined` is render-stable. + */ +const defaultCtx = { dataSource: undefined }; class PreviewErrorBoundary extends Component< { children: ReactNode; signal: unknown }, From e2c31aa1d6c4f5829d3fb23a18f0243aba60524e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 04:00:30 +0000 Subject: [PATCH 3/3] test(components): mark the guide's data-scope injection as the crossing it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Type Check` went red on this branch only after `main` merged in: objectui#8021's `guide-schema-rendering-data-context-8021.test.tsx` injects a plain scope bag as `SchemaRendererProvider.dataSource`, and legs A/C/D exist to measure exactly that — the renderer binds the context's `dataSource` as the expression `data` root. The file landed after this branch's own sweep, so it is the one injection site the sweep could not have marked. Same treatment as the other 33 test files here: an explicit `as unknown as DataSource` at the injection, with the file's own note naming the second meaning. The injected value is byte-for-byte what it was, all six assertions are unchanged, and the declaration is untouched — nothing widens, nothing is skipped. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ --- ...e-schema-rendering-data-context-8021.test.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/components/src/__tests__/guide-schema-rendering-data-context-8021.test.tsx b/packages/components/src/__tests__/guide-schema-rendering-data-context-8021.test.tsx index e9557b3dd9..4435fb1c8e 100644 --- a/packages/components/src/__tests__/guide-schema-rendering-data-context-8021.test.tsx +++ b/packages/components/src/__tests__/guide-schema-rendering-data-context-8021.test.tsx @@ -66,6 +66,20 @@ import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import '../renderers'; import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; +import type { DataSource } from '@object-ui/types'; + +/** + * NOTE (objectui#7912): `SchemaRendererProvider.dataSource` — and the context + * it feeds — declare the published `DataSource` adapter contract. The values + * this file injects are deliberately NOT adapters — + * `SCOPE` is the `data` ROOT of the expression scope, which is exactly what + * this document's Data Context passage teaches and what legs A/C/D measure: + * the renderer binds `SchemaRendererContext.dataSource` as `data` for every + * predicate, the second meaning this one key carries (objectui#9308). + * Each injection therefore crosses the contract with an explicit + * `as unknown as DataSource`. Every injected value is byte-for-byte what it + * was before: this marks the crossing, it changes no assertion. + */ /** * Anchored on this file's own naked `import.meta.url`, never on @@ -108,7 +122,7 @@ function renderWithDataProp(content: string): string { function renderWithProvider(content: string): string { return ( render( - + , ).container.textContent ?? ''