Skip to content

fix(plugin-charts): the schema normalizer and chart registration are reachable through the published surface (#4529) - #4537

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4529-charts-subpath-contract
Aug 13, 2026
Merged

fix(plugin-charts): the schema normalizer and chart registration are reachable through the published surface (#4529)#4537
yinlianghui merged 1 commit into
mainfrom
claude/issue-4529-charts-subpath-contract

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4529.

@object-ui/plugin-charts publishes exactly one entry point, and two plugin-dashboard suites imported subpaths of it. Both resolved only through the repo's vitest alias; under Node's own resolution, or from any consumer outside this repo, both are ERR_PACKAGE_PATH_NOT_EXPORTED. PR #4530 could not fix that from a test-surface card, so it restated the alias as one narrow, commented paths entry in packages/plugin-dashboard/tsconfig.test.json, with the instruction to delete it once this card ruled.

The two imports are different kinds of dependency and get two different dispositions, each measured rather than assumed.

1. normalizeChartSchema — a load-bearing value, published from the root entry

Measured first, per the ruling: is it already reachable from the root entry? No. packages/plugin-charts/src/index.tsx exported ChartBarRenderer, ChartRenderer, ObjectChart, ObjectChartBlock, chartComponents and the BarChartSchema type, and nothing else.

Is the exclusion deliberate — a lazy-loading or bundle-size constraint? No, and this is the measurement that decides it: ChartRenderer.tsx line 5 imports normalizeChartSchema statically, and index.tsx imports ChartRenderer statically. The module is already in the entry's eager graph. Publishing it adds a name, not a byte. So the ruling's fallback (a subpath export for a deliberately-excluded module) does not apply, and the primary branch lands: it is exported from the package root entry, together with the NormalizedChartSchema type it returns so the published function's contract is nameable.

This is a genuine cross-package contract, not a test convenience. The assertions run the widget's emitted schema through the same translation layer the runtime applies, which is exactly what a consumer does; restating that translation in the test instead would replace the test with a copy of the thing under test (#4471).

2. AdvancedChartImpl — the #4325 precedent transfers, and the deep import goes

Measured: how does production load it? One way only. ChartRenderer.tsx:

const LazyAdvancedChart = React.lazy(() => import('./AdvancedChartImpl'));

There is no eager path anywhere in production. The test's side-effect import existed only to pre-warm that chunk so the unbounded first import was paid in the import phase rather than inside RTL's 1000 ms waitFor.

Measured: does the published barrel preload it? No — #4460's refutation of "preload through the public index" reproduces here rather than being cited. After importing @object-ui/plugin-charts, a first dynamic import of the implementation costs 60.3 ms; a genuine cache hit immediately after costs 0.0 ms. The barrel evaluates the module that declares the lazy factory; the factory never runs.

So the surface does not grow to keep an eager deep import alive (#4325, PR #4460). The import is deleted and the test reaches the registration exactly the way production does: render the real chain and await the Suspense boundary.

Measured: what does that cost, and how is it handled? Unlike #4325's case, the race cannot be removed by choosing a better witness — every witness in this file is post-boundary DOM by design, which is the entire point of the file. So it is budgeted instead. On an idle container, render to chart mount takes 359.1 ms with the chunk cold against 91.4 ms when eagerly pre-imported, so dropping the pre-import moves about 270 ms inside that one wait. That fits the 1000 ms default here and would still be a coin flip on a loaded machine — AGENTS.md records first-import latencies up to 976 ms under full parallelism — so the single waitFor that spans the boundary is given an explicit 15 s budget. waitFor polls and returns as soon as the node appears, so the budget costs nothing when the chunk is warm.

No test semantics changed. Same assertions, same witnesses, same renderWidget, in both files. The deleted import was a preload, never a witness; the changed import in DatasetWidget.comboPresentation.test.tsx is the same function reached by its published spelling.

3. The transitional paths entry is deleted

packages/plugin-dashboard/tsconfig.test.json now carries "paths": {}, like every other wired-up package, with the comment rewritten to record what was ruled rather than what was owed.

Red-first

Predicted in writing before running: deleting the paths entry with the imports unchanged reports exactly the two errors #4529 quoted, and nothing else.

src/__tests__/DatasetWidget.chartConfig.dom.test.tsx(38,8): error TS2882: Cannot find module or type declarations for side-effect import of '@object-ui/plugin-charts/AdvancedChartImpl'.
src/__tests__/DatasetWidget.comboPresentation.test.tsx(52,38): error TS2307: Cannot find module '@object-ui/plugin-charts/normalizeChartSchema' or its corresponding type declarations.

Exit 2, error count 2. After the fix, tsc -p tsconfig.test.json exits 0 with "paths": {} — both imports resolving through the published surface.

Reverse verification — direction predicted first, and it is not the obvious one

Reverting all four files returns to origin/main, which was green (the paths entry restated the alias), so that direction proves nothing. The load-bearing question is whether the new published surface is what carries the value import, so the limb removed was packages/plugin-charts/src/index.tsx alone, with the tests' new spellings and the emptied paths left in place. Three predictions, all confirmed:

predicted measured
dist/index.d.ts returns byte-identical to the pre-fix baseline sha256 05539ce9… both times
exactly one error, TS2305 (missing member, not TS2307/TS2882 — the module resolves now) DatasetWidget.comboPresentation.test.tsx(53,10): error TS2305: Module '"@object-ui/plugin-charts"' has no exported member 'normalizeChartSchema'. — 1 error
chartConfig.dom.test.tsx stays silent, since it imports only the barrel for its side effect silent — which is the evidence the two dispositions are genuinely independent

Taken out with git checkout -- and restored from a sha256-verified copy (2c6f2834…, matched after restore), never git stash. Rebuilt and re-checked green afterwards.

The .d.ts diff, measured both ways

dist/ and tsconfig.tsbuildinfo cleared between builds. The entire diff:

4a5,6
> export { normalizeChartSchema } from './normalizeChartSchema';
> export type { NormalizedChartSchema } from './normalizeChartSchema';

Additive only — nothing removed, nothing renamed, no signature narrowed. Measured in the other direction too: removing the limb returns the file to the baseline hash exactly. Minor, never major.

Green, and what must not change

check result
tsc -p tsconfig.test.json (plugin-dashboard tests) exit 0
pnpm --filter @object-ui/plugin-dashboard type-check (both passes) exit 0
pnpm --filter @object-ui/plugin-charts type-check exit 0
vitest run packages/plugin-dashboard/ 47 files, 388 tests passed — the #4530 number, unchanged
vitest run packages/plugin-dashboard/ packages/plugin-charts/ 72 files, 575 tests passed
node scripts/check-type-check-coverage.mjs exit 0 — 41/41 tests compiled, 0 declared debt (#4530's wiring intact)
node scripts/check-control-bytes.mjs exit 0 (4252 files)
node scripts/check-phantom-dependencies.mjs exit 0
check-changeset-presence / -no-major / -fixed exit 0
eslint on all changed files 0 errors

One declared deviation

ESLint gains one new warning, not zero: react-refresh/only-export-components at packages/plugin-charts/src/index.tsx:28, on the new export { normalizeChartSchema } line. Baseline for that file on origin/main is 1 warning of the same rule on the same grounds (its pre-existing chartComponents export); my tree has 2. It is an inherent consequence of adding a value export to a barrel that already trips this rule, and it is left un-suppressed rather than silenced with a disable comment that its identical neighbour does not carry. Zero errors, and the lint workflow deliberately does not set --max-warnings. The two test files are unchanged at 13 warnings / 0 errors, all pre-existing no-explicit-any.

Changeset: @object-ui/plugin-charts minor — entry-reachable additive growth, as ruled. plugin-dashboard's changes are two test files and a test-only tsconfig, which release nothing.


Generated by Claude Code

…reachable through the published surface (#4529)

Two plugin-dashboard suites imported `@object-ui/plugin-charts` subpaths the
package does not publish (its `exports` map declares "." alone). They resolved
only through the repo's vitest alias, and PR #4530 had to restate that alias as
a transitional `paths` entry so tsc could check what vitest loads.

The two imports get two different dispositions, each measured:

- `normalizeChartSchema` is a load-bearing VALUE — the assertions run the
  emitted schema through the same translation the runtime applies. It is now
  exported from the package ROOT entry. Measured: `ChartRenderer` already
  imports it statically, so it was in the entry's eager graph already; this
  publishes a name, not bytes. plugin-charts graded MINOR.

- `AdvancedChartImpl` was an eager pre-warm of a chunk production only ever
  reaches through `React.lazy`, so #4325's ruling (PR #4460) transfers: the deep
  import is dropped rather than the surface growing to keep it alive. The test
  now reaches the registration exactly as production does. Measured: importing
  the published barrel does NOT preload the chunk (60.3 ms first dynamic import
  after the barrel, against 0.0 ms for a real cache hit), so the ~270 ms this
  moves inside the wait is budgeted explicitly instead of raced.

The transitional `paths` entry is deleted; `tsc -p tsconfig.test.json` exits 0
with both imports resolving through the published surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 13, 2026 5:57am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-z0z3Fups.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.32KB 108.45KB
core (index.js) 3.37KB 1.34KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 156.23KB 42.29KB
fields (index.js) 230.14KB 57.12KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 62.10KB 17.67KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 120.95KB 31.53KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.88KB 59.99KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 189.28KB 50.29KB
plugin-kanban (index.js) 48.62KB 13.42KB
plugin-list (index.js) 111.13KB 27.12KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.16KB 10.96KB
plugin-timeline (index.js) 26.25KB 7.53KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.08KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.73KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

PM step-7 复核 — ACCEPT (session_017Qqyix2QcnpUC9XeYVDzx3)

  • The measurement set is the model for this card class: root-entry non-reachability paired with the proof that the exclusion was NOT deliberate (ChartRenderer already imports normalizeChartSchema statically, so the module sits in the entry's eager graph — publishing adds a name, not a byte), which is exactly what the ruling's fallback clause needed answered; test(plugin-detail): drop the unpublished @object-ui/fields deep subpath (#4325) #4460's "barrel does not preload" refutation re-measured here (60.3 ms first dynamic import vs 0.0 ms cache hit) instead of cited; and the exports map probed end-to-end from the consumer package with the old subpaths still refused — the surface gained a name, not subpaths.
  • Both dispositions landed as ruled: root-entry export (+ the NormalizedChartSchema return type so the published contract is nameable) graded MINOR; the eager deep import DELETED per the @object-ui/fields deep subpaths resolve only through the repo vitest alias — its exports map publishes none of them #4325/test(plugin-detail): drop the unpublished @object-ui/fields deep subpath (#4325) #4460 precedent with the test reaching registration exactly as production does, and the unavoidable Suspense wait BUDGETED from measured numbers (359 ms cold vs 91 ms eager → explicit 15 s), not guessed.
  • The reverse verification is worth naming: a full revert returns to green main and proves nothing, so the limb removed was plugin-charts' index alone — yielding TS2305 (missing member, not missing module) and a silent chartConfig file, which together prove the two dispositions independent. Predicted before run, confirmed exactly.
  • Declared deviations all accepted: the +1 react-refresh warning is inherent to a value export in a barrel that already trips the rule (and correctly NOT silenced with a disable its neighbour lacks); the build-command correction (adding plugin-charts itself to the closure) fixed a real gap in my dispatch — without it the green pass would have read a stale declaration; the registering-barrel semantic note is recorded where the next reader will look.
  • The 403-curl background waiter was caught before it could fire a stale wake and replaced with foreground MCP polling in the same step — correct recovery under the standing rule.
  • chore(types): plugin-dashboard's tests compile — the #4040 program closes at 40/40 #4530's transitional paths entry deleted with its comment rewritten to record the ruling; gate still 41/41, 0 debt; 388 dashboard tests byte-stable. Finding finding(react): the built dist emits extensionless relative imports, so plain Node ESM cannot load the published entry #4538 (extensionless relative imports in @object-ui/react's dist — dormant until an unbundled Node ESM consumer) filed properly.

Auto-merge armed (squash) — landing will be verified per the new merge-queue discipline.


Generated by Claude Code


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 13, 2026 06:15
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit 5fac011 Aug 13, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4529-charts-subpath-contract branch August 13, 2026 06:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(plugin-charts): two deep subpaths resolve only through the repo vitest alias — the #4325 shape again, and this one has a value import

2 participants