-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Environment locks #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 2054f8c The changes in this PR will be included in the next version bump. This PR includes changesets to release 24 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces "environment locks" as a boundary mechanism to control when data-override attributes should be introduced in the Bento component system. The change fundamentally shifts the override detection system from always flagging modifications to only flagging them when they cross established lock boundaries, enabling design systems to distinguish between internal composition and consumer customizations.
- Removes the
@bento/internal-propspackage entirely and simplifiesusePropsto work directly with props - Introduces lock boundaries via
Environmentcomponent'slockprop with generation tracking - Updates override detection to respect lock boundaries when determining whether to add
data-overrideattributes
Reviewed Changes
Copilot reviewed 31 out of 32 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
packages/use-props/src/index.ts |
Simplified to remove internal-props dependency and work directly with props |
packages/slots/src/slots.tsx |
Added generation tracking for slot assignments to support lock boundaries |
packages/slots/src/override.ts |
Updated override detection logic to respect lock boundaries and generations |
packages/environment/src/index.tsx |
Added lock prop to create boundaries and increment generation counters |
packages/box/src/index.ts |
Added lock-related properties to context interfaces |
packages/internal-props/* |
Entire package removed |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/slots/src/override.ts
Outdated
| if (!isLocked || slotGeneration < currentLockGeneration) { | ||
| causes.push('className'); | ||
| } |
Copilot
AI
Nov 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The logic for determining when to flag className could be more explicit. Consider extracting this condition into a helper function like shouldFlagModification(isLocked, slotGeneration, currentLockGeneration) to improve readability and make the lock boundary logic more reusable."
packages/slots/src/override.ts
Outdated
| if (!isLocked || slotGeneration < currentLockGeneration) { | ||
| causes.push('style'); | ||
| } |
Copilot
AI
Nov 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] This duplicates the same lock boundary condition as the className check above. Consider extracting this logic into a shared helper function to avoid code duplication."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing there's no deprecation feature in changesets?
| ctx.env.locked = true; | ||
| ctx.env.lockGeneration = ctx.env.lockGeneration + 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a benefit to having both locked and lockGeneration? Wondering if we'd be fine with checking that isNaN(ctx.env.lockGeneration) or ctx.env.lockGeneration < 1?
| const result = container.innerHTML; | ||
|
|
||
| // Should have NO data-override attributes since all slots are internal composition | ||
| assume(result).equals( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is ridiculously huge. Could we not just use a snapshot here instead of doing this? https://vitest.dev/guide/snapshot.html#snapshot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, but also that is how the existing tests are also asserting structure.
So while I would agree that snapshots is a better option, this really is something that should be applied to all tests to set this as a standard going forward, and should be done as a separate, follow-up effort, instead of as part of this PR.
packages/slots/src/override.ts
Outdated
|
|
||
| // Only flag className if environment is NOT locked, or if slot is from earlier generation | ||
| if ('className' in props && !causes.includes('className')) { | ||
| if (!isLocked || slotGeneration < currentLockGeneration) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed the lock boundary condition !isLocked || slotGeneration < currentLockGeneration appears in a couple places. Would it be worth extracting this into a small helper function?
| // If newSlot is from an earlier generation (consumer slot), update the tag | ||
| // We assume parent slots (assignedSlot) are from consumer, so keep their generation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, slightly confused by this comment. It seems like we're preserving the parent's generation (by not overwriting it), rather than updating the tag? Just want to make sure I understand.
packages/slots/src/override.ts
Outdated
| if (slot && isLocked && slotGeneration < currentLockGeneration) { | ||
| // Any slot modification from an earlier generation should be flagged | ||
| if (!causes.includes('slot')) { | ||
| causes.push('slot'); | ||
| } | ||
| // Also add specific triggers if present | ||
| Object.keys(slot).forEach(function forEach(name) { | ||
| if (triggers.includes(name) && !causes.includes(name)) { | ||
| causes.push(name); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick question about the design here: looks like we're flagging both 'slot' AND the specific triggers (className/style), which produces something like data-override="style slot". Is the granularity needed for debugging or tooling purposes? Just curious if data-override="slot" alone would be sufficient, or if having the specifics is valuable.
If the detailed triggers are intentional (which makes sense for debugging!), it might be worth adding a quick comment explaining the reasoning for future maintainers maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great question, there's an important distinction here, class and style are visual overides while a slot can also be a functional override. E.g. you can choose to break/improve/change interaction patterns by overriding a onPress or onClick to a container. Visually, nothing would break, but functionally, it has a completely different impact.
Then if we want to go really deeply here, slots are not the only way to make modifications to components, you could take a component that ends up in your Design System and apply your own className to it, that would also trigger a data-override, but it's not caused by a slot.
But in the end, this detailed differentiation is there purely for a debugging point of view so when you inspect a component that doesn't function according to your pre-defined specifications.
If we feel that this is too verbose, we could also decide to just change the <Environment> api, and just have lock accept an array of triggers. e.g. <Environment lock=['slot'] />
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the clarification. The distinction makes sense, and the granularity would for sure help debugging.
Given this, should we consider adding a brief comment in the code explaining why both 'slot' and the specific triggers are included.
Regarding <Environment lock={['slot']} />, would that allow consumers to opt into less granular override tracking if they prefer? Or would it change the behavior entirely?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given this, should we consider adding a brief comment in the code explaining why both 'slot' and the specific triggers are included.
Yes, i'll update the comments 👍
Regarding <Environment lock={['slot']} />, would that allow consumers to opt into less granular override tracking if they prefer? Or would it change the behavior entirely?
The Environment and locking would be something that we would apply as part of the components that we offer in our Design System. So I don't know how much sense it makes to add, but from an OS perspective, having the customization could be nice, just not sure if it's worth the squeeze.
- resolved conflicts by keeping internal-props package deleted - updated use-props package.json to remove internal-props dependency - regenerated package-lock.json
Resolved conflicts by: - Removed @bento/internal-props dependency (kept in environment-locks) - Added @react-aria/utils for ref merging - Integrated ref handling logic from main without internal-props - Regenerated package-lock.json
…ironment-locks * 'environment-locks' of github.com:godaddy/bento:
- Convert use-props, slots, and environment browser tests to use toMatchSnapshot() - Add withLock HOC for wrapping components with locked Environment - Fix use-props node tests to use locked environment when testing data-override - Add @bento/environment and @bento/use-props as devDependencies for slots tests
d750bfb to
5fd7f75
Compare
- Convert portal tests to use vitest snapshots - Fix TypeScript errors in box context tests (add locked, lockGeneration, slotGenerations) - Update knip config to ignore .ts test files for circular dependency detection - Remove circular dependency from slots package.json
Environment lock={true} can be used directly instead.
- Add tests for slot generation tagging on first/nested lock (100% coverage) - Fix TypeScript errors in lock-no-override example (cast components to accept children)
Introducing environment/boundary locks to flag when
data-overrideshould be introduced.Checklist
npm run changesetif this affects packages)