Skip to content

fix(peers): stop the barrel from requiring optional peer dependencies#1737

Merged
marblom007 merged 3 commits into
masterfrom
fix/optional-peer-module-scope-imports
Jul 22, 2026
Merged

fix(peers): stop the barrel from requiring optional peer dependencies#1737
marblom007 merged 3 commits into
masterfrom
fix/optional-peer-module-scope-imports

Conversation

@marblom007

@marblom007 marblom007 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #1735. Completes the optional-peer work started in #1732.

The bug class

@mui/x-date-pickers and date-fns are both declared optional under peerDependenciesMeta, so a consumer is entitled to skip them. But src/index.tsx re-exports nearly everything, which means a module-scope import of an optional peer in any barrel-reachable file breaks that promise for everyone - not just for people who render the component that needs it:

import { Button } from '@sistent/sistent';
// Error: Cannot find module 'date-fns'

The message names sistent rather than the peer the consumer deliberately did not install, which is what makes it expensive to diagnose downstream.

#1732 fixed one instance by deferring @mui/x-date-pickers to React.lazy. One instance remained: UniversalFilter still imported subDays / subMonths / subYears from date-fns at module scope for its quick-range presets, so the barrel was still unusable on a clean install. Meshery UI happens to have date-fns installed and never saw it; Kanvas does not, and is blocked on this (layer5labs/meshery-extensions#4303).

The fix

New src/utils/date.utils.ts - subtractDays / subtractMonths / subtractYears, dependency-free.

Deliberately a leaf module with no imports at all, and deliberately not folded into time.utils.tsx, which pulls in moment and a React component: pure date arithmetic should be reachable without dragging a rendering stack behind it. moment was considered and rejected for the same reason - swapping one heavyweight import for another does not fix an import-graph problem.

Semantics match date-fns exactly:

  • end-of-month clamping - subtractMonths(May 31, 3) is Feb 28, not Mar 3 (plain setMonth overflows)
  • subtractYears(Feb 29 2024, 1) is Feb 28 2023, not Mar 1
  • local-time day arithmetic rather than n * 86400000 ms, so a range spanning a DST boundary keeps its wall-clock time instead of drifting an hour

Preventing recurrence

src/__testing__/optionalPeerDependencies.test.ts guards the whole class, not this one component. It walks the source tree and fails on any eager import of an optional peer, listing the offending paths - so the failure message is also the remediation. It additionally asserts the peers are still marked optional, so if that premise ever changes the assertions get revisited rather than silently kept.

This matters because, as noted in #1736, CI cannot otherwise catch this: the repo always has its own devDependencies installed, so the breakage only appears in a downstream clean install. This test makes it visible in-repo.

src/__testing__/date.utils.test.ts covers the helpers themselves, including the clamping and DST cases above.

Also carried in this change

  • DateTimePicker wraps its lazy peer import so a missing peer surfaces as a message naming the peer, rather than a raw module-not-found thrown mid-render from inside a promise. Deferring the resolution deferred the failure too, and an unactionable async crash is no improvement on an unactionable sync one.
  • time.utils.tsx imports CustomTooltip from its leaf path instead of the ../custom barrel. That closed a custom/X -> utils -> custom/index -> X cycle and forced every consumer of a date helper to load the entire component library (including, via Markdown, ESM-only react-markdown).
  • permissions.tsx types createCanShow's bus as a structural ReasonEventPublisher. EventBus<T> is invariant in T - it both accepts T in publish and yields T from on - so the host's EventBus<MesheryExtensionEvent>, the bus the contract tells every host to declare, was not assignable to EventBus<ReasonEvent> and the integration failed to compile. It also publishes via the named MESHERY_EXTENSION_EVENT.MissingPermission handle so a contract rename breaks this publish site rather than silently ceasing to match the subscriber across the bundle boundary, and drops a stray console.log from the click path.
  • Barrel exports HasKeyProps, InvertAction, ReasonEvent and ReasonEventPublisher alongside createCanShow; without them a consumer cannot name the types of the wrapper it builds around the returned component and falls straight back to any.

Verification

  • npm test - 24 suites, 443 tests passing, including the 32 new ones
  • npm run lint clean; prettier --check clean on every touched file (the ~82 pre-existing failures elsewhere are unrelated, see docs: add AGENTS.md with release, optional-peer, and repo-state notes #1736)
  • npm run build clean
  • By content, on the built output: the string date-fns no longer appears as a module specifier anywhere in dist/index.js or dist/index.mjs. The three @mui/x-date-pickers/* specifiers that remain are inside import(...) calls only - no static import survives.
  • Differential against the real library: 167,637 comparisons of subtractDays / subtractMonths / subtractYears against date-fns@4.4.0 - every day from 2019-2027 at three times of day across a range of amounts - produced 0 mismatches. The "semantics match exactly" claim is measured, not asserted.
  • End to end: with node_modules/date-fns and node_modules/@mui/x-date-pickers both removed, require('./dist/index.js') succeeds and resolves 719 exports. That is the exact scenario that throws on master today.

Guard hardening (from review)

The first version of the guard only matched import ... from 'x' and require('x'). Review caught that it missed several forms that resolve the module just as eagerly, and one of them mattered a great deal:

  • export ... from 'x' - src/index.tsx is built almost entirely out of export ... from. The barrel is precisely where this bug class originates, and the guard was blind to the barrel's own syntax. Adding export { subDays } from 'date-fns' to src/index.tsx passed the original guard; it now fails and names index.tsx.
  • import 'x' - a side-effect import binds nothing, which is exactly why it is easy to miss by eye.
  • the subpath variants of both.

The detector is now pinned by its own cases (13 assertions for what it must and must not flag: dynamic import(), type-only imports and re-exports, unrelated specifiers, prose mentions). A scanner that quietly stops matching a form reports the same clean result as a clean codebase, so the pattern needed to stop being the untested part of the test.

import type stripping was widened to export type to match - otherwise the new export ... from branch would fail a correct type re-export, and the only way to satisfy the guard would be to delete a type.

Separately, react and react-dom are also marked optional in package.json but are not scanned (every component imports React at module scope and always will). They are now named in an explicit exemption list, and a new assertion requires the scanned set plus the exemptions to equal the declared optional peers exactly - so marking a new peer optional fails the suite rather than silently creating a peer nothing guards. Verified by mutation with @rjsf/core.

Also fixed: the DateTimePicker remediation message named a <UniversalFilter dateRange> prop that does not exist (it is datePicker). An error message whose entire purpose is to be actionable has to name the real API.

Out of scope, but noticed

dist/index.mjs emits import ... from "lodash/debounce" (from src/custom/SearchBar.tsx), which Node's ESM resolver rejects - it needs the .js extension. Bundlers paper over it, so it does not affect Meshery UI or Kanvas, and it is pre-existing on master and unrelated to optional peers. Filed separately rather than folded in here.

Note for reviewers

master picked up two contract commits (5eeff36, 61619b2) after the base this work was written against, which refined mesheryExtensionContract.ts and its tests along a different line. Those landed versions are kept as-is here - this PR does not touch either file, so nothing already on master is reverted.

#1736 lists #1735 as a "known live instance" of the barrel trap; that line goes stale once this merges and is worth a one-line update there.

Summary by CodeRabbit

  • New Features
    • Added dependency-free date helpers (subtractDays, subtractMonths, subtractYears) that return new Date values, preserve time-of-day, and clamp to valid month-end/leap-year dates.
    • Updated quick date-range filters and exported the new date utilities.
  • Bug Fixes
    • Improved optional date-picker peer dependency handling with clearer, deferred load errors.
    • Standardized missing-permission event typing and re-exported permission types from the main entrypoint.
  • Tests
    • Added Jest coverage for date subtraction edge cases and enforced optional peer dependency import safety.

`@mui/x-date-pickers` and `date-fns` are declared optional under
`peerDependenciesMeta`, so a consumer is entitled to skip them. But
`src/index.tsx` re-exports nearly everything, which means a module-scope
import of an optional peer in ANY barrel-reachable file breaks that promise
for everyone: `import { Button } from '@sistent/sistent'` throws
`Cannot find module 'date-fns'` before a single component renders, and the
message names sistent rather than the peer the consumer deliberately did not
install.

#1732 fixed one instance of this class by deferring `@mui/x-date-pickers` to
`React.lazy`. `UniversalFilter` was still importing `subDays`/`subMonths`/
`subYears` from `date-fns` at module scope for its quick-range presets, so
the barrel remained unusable on a clean install (#1735). Meshery UI happens
to have `date-fns` installed and never saw it; consumers that do not, do.

The presets now use `src/utils/date.utils.ts`, a new dependency-free leaf
module. It is deliberately NOT folded into `time.utils.tsx`, which drags in
`moment` and a React component - pure date arithmetic should be reachable
without a rendering stack behind it. Semantics match `date-fns` exactly,
including end-of-month clamping (`subtractMonths(May 31, 3)` is Feb 28, not
Mar 3) and local-time day arithmetic so a range spanning a DST boundary does
not drift by an hour.

`optionalPeerDependencies.test.ts` guards the whole bug class rather than
this one component: it walks the source tree and fails on any eager import
of an optional peer, listing the offending paths so the failure message is
also the remediation. It also asserts the peers are still marked optional,
so if that premise ever changes the assertions get revisited rather than
silently kept.

Also carried in this change:

- `DateTimePicker` now wraps its lazy peer import so a missing peer surfaces
  as a message naming the peer, instead of a raw module-not-found thrown
  mid-render from inside a promise. Deferring the resolution deferred the
  failure too; an unactionable async crash is no improvement on an
  unactionable sync one.
- `time.utils.tsx` imports `CustomTooltip` from its leaf path instead of the
  `../custom` barrel, breaking a `custom/X -> utils -> custom/index -> X`
  cycle and no longer forcing consumers of a date helper to load the entire
  component library.
- `permissions.tsx` types `createCanShow`'s bus as a structural
  `ReasonEventPublisher`. `EventBus<T>` is invariant in `T`, so the host's
  `EventBus<MesheryExtensionEvent>` - the bus the contract tells every host
  to declare - was not assignable to `EventBus<ReasonEvent>` and the
  integration failed to compile. It also publishes via the named
  `MESHERY_EXTENSION_EVENT.MissingPermission` handle so a contract rename
  breaks this publish site rather than silently ceasing to match the
  subscriber, and drops a stray `console.log` from the click path.
- The barrel exports `HasKeyProps`, `InvertAction`, `ReasonEvent` and
  `ReasonEventPublisher` alongside `createCanShow`; without them a consumer
  cannot name the types of the wrapper it builds and falls back to `any`.

Fixes #1735

Signed-off-by: Ahmed Jamil <197998703+CodeAhmedJamil@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 22, 2026 04:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds dependency-free date arithmetic, removes eager date-fns usage, adds optional-peer dependency regression checks and clearer lazy-loading errors, narrows permission event typing, and explicitly re-exports related permission types.

Changes

Optional dependency and date handling

Layer / File(s) Summary
Dependency-free date range utilities
src/utils/date.utils.ts, src/utils/index.ts, src/custom/UniversalFilter.tsx, src/__testing__/date.utils.test.ts
Adds calendar subtraction helpers with end-of-month clamping, integrates them into quick date ranges, and tests boundary behavior and immutability.
Optional peer loading safeguards
src/base/DateTimePicker/DateTimePicker.tsx, src/__testing__/optionalPeerDependencies.test.ts, src/utils/time.utils.tsx
Adds deferred optional-peer error handling, verifies optional peers are not eagerly imported, and replaces a barrel import with a leaf import.

Permission event contract and exports

Layer / File(s) Summary
Permission event contract and public exports
src/custom/permissions.tsx, src/index.tsx
Introduces ReasonEventPublisher, uses the named missing-permission event constant, removes logging, and explicitly re-exports permission types and values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

  • layer5io/sistent#1732 — Both changes update optional-peer handling and permission typing around the shared extension contract.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preventing the package barrel from eagerly requiring optional peer dependencies.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/optional-peer-module-scope-imports

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/__testing__/optionalPeerDependencies.test.ts`:
- Around line 20-28: The eagerImportPattern must also match side-effect imports
such as import 'date-fns' and re-exports such as export { x } from 'date-fns'.
Update the pattern’s import syntax alternatives while preserving existing
require(), named/default import, specifier escaping, subpath matching, and
exclusion of await import().

In `@src/base/DateTimePicker/DateTimePicker.tsx`:
- Around line 32-36: Update the remediation message in the DateTimePicker error
construction to reference the UniversalFilter datePicker prop instead of
dateRange, while preserving the existing optional-dependency guidance and error
cause.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 40a326ba-529c-4d7a-9621-d858b96878e1

📥 Commits

Reviewing files that changed from the base of the PR and between 46dcc81 and 99934cb.

📒 Files selected for processing (9)
  • src/__testing__/date.utils.test.ts
  • src/__testing__/optionalPeerDependencies.test.ts
  • src/base/DateTimePicker/DateTimePicker.tsx
  • src/custom/UniversalFilter.tsx
  • src/custom/permissions.tsx
  • src/index.tsx
  • src/utils/date.utils.ts
  • src/utils/index.ts
  • src/utils/time.utils.tsx

Comment thread src/__testing__/optionalPeerDependencies.test.ts Outdated
Comment thread src/base/DateTimePicker/DateTimePicker.tsx
The guard only matched `import ... from 'x'` and `require('x')`, so three
forms that resolve the module just as eagerly slipped past it:

- `export ... from 'x'` - and this is the one that matters, because
  `src/index.tsx` is built almost entirely out of `export ... from`. The
  barrel is precisely where this bug class originates, and the guard was
  blind to the barrel's own syntax.
- `import 'x'` - a side-effect import binds nothing, which is exactly why it
  is easy to miss by eye and why a scanner should catch it.
- the subpath variants of both.

A guard that silently stops matching reports the same clean result as a clean
codebase, so the detector is now pinned by its own cases: thirteen assertions
covering each form it must flag and each it must not - dynamic `import()`,
type-only imports and re-exports, unrelated specifiers, and prose mentions.
Verified by mutation: adding `export { subDays } from 'date-fns'` to
`src/index.tsx` now fails the suite and names `index.tsx`, where before it
passed.

`import type` stripping was widened to `export type` to match. Without it the
new `export ... from` branch would fail a correct type re-export, and the only
way to satisfy the guard would be to delete a type.

Also corrects the DateTimePicker remediation message, which named a
`<UniversalFilter dateRange>` prop that does not exist - the prop is
`datePicker`. An error message whose purpose is to be actionable has to name
the real API.

Signed-off-by: Ahmed Jamil <197998703+CodeAhmedJamil@users.noreply.github.com>
@marblom007

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

The guard checked that the two peers it scans are still marked optional, but
not the converse: marking a *third* peer optional created a peer nothing
scanned, and the suite kept passing while the bug class quietly reopened
behind it. The list of scanned peers and the optional peers declared in
package.json now have to agree exactly.

`react` and `react-dom` are also marked optional and are named in an explicit
exemption list rather than skipped silently - every component imports React at
module scope and always will, so scanning for it would fail on the whole tree.
Recording them as a decision keeps the completeness check meaningful instead
of making it something to loosen the first time it fires.

Verified by mutation: marking `@rjsf/core` optional now fails the suite and
names it.

Signed-off-by: Ahmed Jamil <197998703+CodeAhmedJamil@users.noreply.github.com>
@marblom007

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@marblom007
marblom007 merged commit fa0b8f1 into master Jul 22, 2026
6 checks passed
@marblom007
marblom007 deleted the fix/optional-peer-module-scope-imports branch July 22, 2026 05:14
marblom007 pushed a commit that referenced this pull request Jul 22, 2026
Two corrections to the optional-peer section.

'CI cannot see this' went stale when #1737 landed
src/__testing__/optionalPeerDependencies.test.ts, which scans src/ for
load-time imports of an enforced optional peer and fails jest. Describe the two
guards as complementary instead: the test covers source, the packed-consumer
check covers what the bundler emitted and what is actually on npm - neither
subsumes the other. Point at the test as the source of truth for which peers
are enforced, since it is where the react / react-dom exemption is recorded.

The absence check only looked for date-fns and only warned, so it could print a
warning and then report a passing load with @mui/x-date-pickers still installed
- a false pass in exactly the situation the check exists to catch. It now walks
the enforced peers and exits nonzero.

Deliberately not enumerating every peer under peerDependenciesMeta: react and
react-dom are marked optional there but are imported at module scope by every
component, so requiring their absence could never pass. That exemption is
documented in the test rather than invented here.

Verified verbatim against the published 0.21.40 tarball: clean consumer loads
(exit 0); with date-fns force-installed it aborts (exit 1).

Signed-off-by: Ahmed Jamil <197998703+CodeAhmedJamil@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Barrel import still requires the optional peer date-fns (UniversalFilter), breaking clean-install consumers

3 participants