Skip to content

Commit 8acdd87

Browse files
committed
fix(plugin-view,plugin-list): hand-list FLAT_MAP_CONFIG_KEYS to stop breaking the console-starter alias-closure gate
PR #5231's CI (job `Test (shard 3/4)`, run 32160288416) went red on `examples/console-starter/test/vite-alias-closure.test.ts`: "@object-ui/types/zod (unresolvable under src, imported by packages/plugin-list/src/ListView.tsx)" "@object-ui/types/zod (unresolvable under src, imported by packages/plugin-view/src/ObjectView.tsx)" Root cause: that test's walker resolves a bare `@object-ui/*` specifier using plain `index.<ext>` conventions and cannot find `@object-ui/types/zod`'s real barrel file (`zod/index.zod.ts` — a non-standard name), regardless of whether Vite's own alias table covers it (it already does — an explicit `@object-ui/types/zod` entry has been in `examples/console-starter/vite.config.ts` since PR #5156). `ObjectView.tsx` and `ListView.tsx` are both reachable from console-starter's own `src/`, so the runtime import made them part of the walked graph for the first time; `ObjectMap.tsx` carries the identical import today without tripping the gate only because nothing in that graph reaches `@object-ui/plugin-map`. Per instruction, `vite-alias-closure.test.ts` itself is untouched. Fix: `FLAT_MAP_CONFIG_KEYS` in `ObjectView.tsx` and `ListView.tsx` is now HAND-LISTED (no runtime `@object-ui/types/zod` import in either file) rather than derived from `ObjectMapConfigSchema.shape`. `ObjectMap.tsx` in `plugin-map` keeps the runtime-derived version — it is safely unreached by the walker, and it already imports the schema for `safeParse` regardless. Anti-drift for the two hand lists is a TEST, not just a comment, matching the established `packages/core/src/actions/__tests__/actionKeys.pin.test.ts` pattern: each of `ObjectView.mapFlatten.test.tsx` / `ListView.mapFlatten.test.tsx` now imports `ObjectMapConfigSchema` from `@object-ui/types/zod` (a TEST file, which the alias-closure walker explicitly excludes from traversal) and asserts the exported `FLAT_MAP_CONFIG_KEYS` equals the schema's keys minus `style`. A key added to or removed from the declaration still fails loudly, by name, without reintroducing the runtime edge that breaks the console-starter gate. Both `FLAT_MAP_CONFIG_KEYS` constants are now independently hand-listed (each pin-tested against the same canonical schema) rather than one literal array physically shared by both call sites — the walker constraint above rules out every within-scope way to share one runtime value without either re-tripping this gate or statically pulling `@object-ui/plugin-map`'s `maplibre-gl` / `react-map-gl` weight into `ObjectView` / `ListView` (that package's single lib entry bundles everything together, and console-starter aliases it whole to `src`). Noted explicitly in the PR body as a deviation from strict single-sourcing, with the reasoning above. Reverified locally: `pnpm exec vitest run examples/console-starter/test/vite-alias-closure.test.ts` green, plus the three package suites, at the new HEAD, rebased onto current origin/main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
1 parent d77dd1e commit 8acdd87

4 files changed

Lines changed: 121 additions & 31 deletions

File tree

‎packages/plugin-list/src/ListView.tsx‎

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ import { UserFilters } from './UserFilters';
1818
import { SchemaRenderer, useNavigationOverlay, classifyLoadError } from '@object-ui/react';
1919
import type { LoadErrorKind } from '@object-ui/react';
2020
import { useDensityMode } from '@object-ui/react';
21-
import type { ListViewSchema } from '@object-ui/types';
21+
import type { ListViewSchema, ObjectMapConfig } from '@object-ui/types';
2222
import { detectStatusField } from '@object-ui/types';
23-
import { ObjectMapConfigSchema } from '@object-ui/types/zod';
2423
import { usePullToRefresh } from '@object-ui/mobile';
2524
import { resolveConditionalFormatting, buildExpandFields, buildExportFileName, resolveEffectiveCrudAffordances, isObjectInlineEditable, normalizeListViewSchema, rowHeightToDensityMode, mergeFilterNodes, columnIdentity, collectPredicateFieldRefs, listViewPredicates, PLATFORM_RECORD_COLUMNS, EXPANDABLE_FIELD_TYPES, UNMATERIALIZED_FIELD_TYPES } from '@object-ui/core';
2625
import { useObjectTranslation, useObjectLabel, useSafeFieldLabel, createSafeTranslation } from '@object-ui/i18n';
@@ -34,17 +33,37 @@ import { usePermissions } from '@object-ui/permissions';
3433
* spreading the raw `map` block collapsed the two namespaces onto one key
3534
* (objectui#5177).
3635
*
37-
* DERIVED, not hand-listed: `ObjectMapConfigSchema` (`@object-ui/types/zod`) is
38-
* the same declaration `plugin-map`'s `ObjectMap.tsx` validates the `map` block
39-
* against and derives its own `FLAT_MAP_CONFIG_KEYS` from, so a key added there
40-
* reaches this flattener without a second edit. Read from `@object-ui/types`
41-
* rather than imported from `@object-ui/plugin-map` deliberately: `plugin-map`
42-
* is loaded lazily (`ComponentRegistry.registerLazy('object-map', ...)`)
43-
* specifically so its `maplibre-gl` / `react-map-gl` weight is paid only when a
44-
* map view actually renders — a static import here would defeat that for every
45-
* `ListView`, map or not.
36+
* HAND-LISTED, not derived at runtime — deliberately, and only here (`plugin-
37+
* map`'s own `FLAT_MAP_CONFIG_KEYS` in `ObjectMap.tsx` DOES derive from
38+
* `ObjectMapConfigSchema.shape`, and should stay that way): this file is
39+
* reachable from `examples/console-starter`'s own `src/`, so it is part of the
40+
* import graph `vite-alias-closure.test.ts` walks. That walker resolves a bare
41+
* `@object-ui/*` specifier with plain `index.<ext>` conventions and cannot find
42+
* `@object-ui/types/zod`'s actual barrel file (`zod/index.zod.ts` — a
43+
* non-standard name) — a REAL runtime import of it here reproducibly fails
44+
* that gate (measured on objectui#5177's first PR, PR #5231, CI run
45+
* 32160288416), even though Vite's own alias table already resolves the
46+
* specifier correctly (`examples/console-starter/vite.config.ts` has carried
47+
* an explicit `@object-ui/types/zod` entry since PR #5156). `ObjectMap.tsx`
48+
* gets away with the runtime import only because nothing in
49+
* console-starter's graph reaches `@object-ui/plugin-map` today.
50+
*
51+
* Anti-drift is a TEST, not this comment: `ListView.mapFlatten.test.tsx` pins
52+
* this exact list against `ObjectMapConfigSchema.shape` — imported only from
53+
* that TEST file, which the alias-closure walker explicitly excludes from
54+
* traversal — so a key added to or removed from the declaration still fails
55+
* here, loudly and by name, without reintroducing the runtime edge that
56+
* breaks the walker.
4657
*/
47-
const FLAT_MAP_CONFIG_KEYS = Object.keys(ObjectMapConfigSchema.shape).filter((key) => key !== 'style');
58+
export const FLAT_MAP_CONFIG_KEYS = [
59+
'latitudeField',
60+
'longitudeField',
61+
'locationField',
62+
'titleField',
63+
'descriptionField',
64+
'zoom',
65+
'center',
66+
] as const satisfies readonly (keyof Omit<ObjectMapConfig, 'style'>)[];
4867

4968
/** Pick only the declared flat map keys present on an authored `map` block. */
5069
function pickFlatMapConfig(mapConfig: unknown): Record<string, unknown> {

‎packages/plugin-list/src/__tests__/ListView.mapFlatten.test.tsx‎

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,28 @@
1616
* legal on every node), so a `map: { style: '<url>' }` authoring intent
1717
* arrived at the top level as that CSS-shaped key.
1818
*
19-
* These pin the whitelist (`FLAT_MAP_CONFIG_KEYS`, derived from
20-
* `ObjectMapConfigSchema` in `@object-ui/types/zod` — the same declaration
21-
* `ObjectMap.tsx`'s own `FLAT_MAP_CONFIG_KEYS` derives from) rather than the
22-
* raw spread: a declared key still travels, `style` does not, and neither
23-
* does an arbitrary undeclared one. Mirrors
24-
* `ObjectView.mapFlatten.test.tsx`'s #5177 coverage for the sibling
19+
* These pin the whitelist (`FLAT_MAP_CONFIG_KEYS`, hand-listed in
20+
* `ListView.tsx` itself — see the comment on that constant for why it is not
21+
* derived from `ObjectMapConfigSchema` at runtime there) rather than the raw
22+
* spread: a declared key still travels, `style` does not, and neither does an
23+
* arbitrary undeclared one. The bottom of this file also pins the hand list
24+
* against `ObjectMapConfigSchema` directly, so it cannot silently drift.
25+
* Mirrors `ObjectView.mapFlatten.test.tsx`'s #5177 coverage for the sibling
2526
* flattener.
2627
*/
2728

2829
import React from 'react';
2930
import { describe, it, expect, vi, beforeEach } from 'vitest';
3031
import { ComponentRegistry } from '@object-ui/core';
3132
import { render, waitFor } from '@testing-library/react';
32-
import { ListView } from '../ListView';
33+
import { ListView, FLAT_MAP_CONFIG_KEYS } from '../ListView';
3334
import { SchemaRendererProvider } from '@object-ui/react';
35+
// Test-only: `ListView.tsx` hand-lists `FLAT_MAP_CONFIG_KEYS` rather than
36+
// importing this schema at runtime, precisely so this module never appears in
37+
// `examples/console-starter`'s walked import graph (see the comment on the
38+
// constant). A test file is explicitly excluded from that walk, so pinning
39+
// against the real declaration HERE is safe.
40+
import { ObjectMapConfigSchema } from '@object-ui/types/zod';
3441

3542
let captured: Array<Record<string, any>> = [];
3643

@@ -114,3 +121,22 @@ describe('ListView flattens `options.map` through a whitelist, not a raw spread
114121
expect(schema.locationField).toBe('location');
115122
});
116123
});
124+
125+
/**
126+
* `FLAT_MAP_CONFIG_KEYS` is hand-listed in `ListView.tsx` itself (not derived
127+
* at runtime — see the comment on the constant for why), so THIS is the
128+
* mechanism that keeps it from silently drifting off `ObjectMapConfigSchema`
129+
* — the same role `packages/core/src/actions/__tests__/actionKeys.pin.test.ts`
130+
* plays for `SPEC_ACTION_KEYS`. A key added to or removed from the schema
131+
* fails this test by name, without requiring a runtime import of
132+
* `@object-ui/types/zod` from production code that reaches
133+
* `examples/console-starter`.
134+
*/
135+
describe('FLAT_MAP_CONFIG_KEYS pins against ObjectMapConfigSchema (objectui#5177)', () => {
136+
it('matches the schema minus `style`, so the hand list cannot silently drift', () => {
137+
const declared = Object.keys(ObjectMapConfigSchema.shape)
138+
.filter((key) => key !== 'style')
139+
.sort();
140+
expect([...FLAT_MAP_CONFIG_KEYS].sort()).toEqual(declared);
141+
});
142+
});

‎packages/plugin-view/src/ObjectView.tsx‎

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type {
3232
ViewType,
3333
NamedListView,
3434
ViewNavigationConfig,
35+
ObjectMapConfig,
3536
} from '@object-ui/types';
3637
import { ObjectGrid } from '@object-ui/plugin-grid';
3738
import { ObjectForm } from '@object-ui/plugin-form';
@@ -58,7 +59,6 @@ import { Plus } from 'lucide-react';
5859
import { useObjectTranslation, createSafeTranslation } from '@object-ui/i18n';
5960
import { buildExpandFields, normalizeListViewSchema, mergeFilterNodes } from '@object-ui/core';
6061
import { SchemaRenderer as ImportedSchemaRenderer } from '@object-ui/react';
61-
import { ObjectMapConfigSchema } from '@object-ui/types/zod';
6262
import { ViewSwitcher } from './ViewSwitcher';
6363
import { deriveRecordSurface } from './recordSurface';
6464

@@ -75,17 +75,37 @@ const SchemaRendererComponent: React.FC<any> = ImportedSchemaRenderer;
7575
* spreading the raw `map` block collapsed the two namespaces onto one key
7676
* (objectui#5177).
7777
*
78-
* DERIVED, not hand-listed: `ObjectMapConfigSchema` (`@object-ui/types/zod`) is
79-
* the same declaration `plugin-map`'s `ObjectMap.tsx` validates the `map` block
80-
* against and derives its own `FLAT_MAP_CONFIG_KEYS` from, so a key added there
81-
* reaches this flattener without a second edit. Read from `@object-ui/types`
82-
* rather than imported from `@object-ui/plugin-map` deliberately: `plugin-map`
83-
* is loaded lazily (`ComponentRegistry.registerLazy('object-map', ...)`)
84-
* specifically so its `maplibre-gl` / `react-map-gl` weight is paid only when a
85-
* map view actually renders — a static import here would defeat that for every
86-
* `ObjectView`, map or not.
78+
* HAND-LISTED, not derived at runtime — deliberately, and only here (`plugin-
79+
* map`'s own `FLAT_MAP_CONFIG_KEYS` in `ObjectMap.tsx` DOES derive from
80+
* `ObjectMapConfigSchema.shape`, and should stay that way): this file is
81+
* reachable from `examples/console-starter`'s own `src/`, so it is part of the
82+
* import graph `vite-alias-closure.test.ts` walks. That walker resolves a bare
83+
* `@object-ui/*` specifier with plain `index.<ext>` conventions and cannot find
84+
* `@object-ui/types/zod`'s actual barrel file (`zod/index.zod.ts` — a
85+
* non-standard name) — a REAL runtime import of it here reproducibly fails
86+
* that gate (measured on objectui#5177's first PR, PR #5231, CI run
87+
* 32160288416), even though Vite's own alias table already resolves the
88+
* specifier correctly (`examples/console-starter/vite.config.ts` has carried
89+
* an explicit `@object-ui/types/zod` entry since PR #5156). `ObjectMap.tsx`
90+
* gets away with the runtime import only because nothing in
91+
* console-starter's graph reaches `@object-ui/plugin-map` today.
92+
*
93+
* Anti-drift is a TEST, not this comment: `ObjectView.mapFlatten.test.tsx`
94+
* pins this exact list against `ObjectMapConfigSchema.shape` — imported only
95+
* from that TEST file, which the alias-closure walker explicitly excludes
96+
* from traversal — so a key added to or removed from the declaration still
97+
* fails here, loudly and by name, without reintroducing the runtime edge that
98+
* breaks the walker.
8799
*/
88-
const FLAT_MAP_CONFIG_KEYS = Object.keys(ObjectMapConfigSchema.shape).filter((key) => key !== 'style');
100+
export const FLAT_MAP_CONFIG_KEYS = [
101+
'latitudeField',
102+
'longitudeField',
103+
'locationField',
104+
'titleField',
105+
'descriptionField',
106+
'zoom',
107+
'center',
108+
] as const satisfies readonly (keyof Omit<ObjectMapConfig, 'style'>)[];
89109

90110
/** Pick only the declared flat map keys present on an authored `map` block. */
91111
function pickFlatMapConfig(mapConfig: unknown): Record<string, unknown> {

‎packages/plugin-view/src/__tests__/ObjectView.mapFlatten.test.tsx‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,14 @@
3030

3131
import { describe, it, expect, vi } from 'vitest';
3232
import { render, waitFor } from '@testing-library/react';
33-
import { ObjectView } from '../ObjectView';
33+
import { ObjectView, FLAT_MAP_CONFIG_KEYS } from '../ObjectView';
3434
import type { ObjectViewSchema } from '@object-ui/types';
35+
// Test-only: `ObjectView.tsx` hand-lists `FLAT_MAP_CONFIG_KEYS` rather than
36+
// importing this schema at runtime, precisely so this module never appears in
37+
// `examples/console-starter`'s walked import graph (see the comment on the
38+
// constant). A test file is explicitly excluded from that walk, so pinning
39+
// against the real declaration HERE is safe.
40+
import { ObjectMapConfigSchema } from '@object-ui/types/zod';
3541

3642
/** Every schema the view hands to SchemaRenderer, in order. */
3743
const rendered: any[] = [];
@@ -148,3 +154,22 @@ describe('ObjectView flattens `map` through a whitelist, not a raw spread (objec
148154
expect(Object.prototype.hasOwnProperty.call(schema, 'totallyUndeclaredKey')).toBe(false);
149155
});
150156
});
157+
158+
/**
159+
* `FLAT_MAP_CONFIG_KEYS` is hand-listed in `ObjectView.tsx` itself (not
160+
* derived at runtime — see the comment on the constant for why), so THIS is
161+
* the mechanism that keeps it from silently drifting off `ObjectMapConfigSchema`
162+
* — the same role `packages/core/src/actions/__tests__/actionKeys.pin.test.ts`
163+
* plays for `SPEC_ACTION_KEYS`. A key added to or removed from the schema
164+
* fails this test by name, without requiring a runtime import of
165+
* `@object-ui/types/zod` from production code that reaches
166+
* `examples/console-starter`.
167+
*/
168+
describe('FLAT_MAP_CONFIG_KEYS pins against ObjectMapConfigSchema (objectui#5177)', () => {
169+
it('matches the schema minus `style`, so the hand list cannot silently drift', () => {
170+
const declared = Object.keys(ObjectMapConfigSchema.shape)
171+
.filter((key) => key !== 'style')
172+
.sort();
173+
expect([...FLAT_MAP_CONFIG_KEYS].sort()).toEqual(declared);
174+
});
175+
});

0 commit comments

Comments
 (0)