Skip to content

Open Service: Add internal property to control visibility#35057

Merged
JReinhold merged 5 commits into
nextfrom
jeppe-cursor/0e545a17
Jun 8, 2026
Merged

Open Service: Add internal property to control visibility#35057
JReinhold merged 5 commits into
nextfrom
jeppe-cursor/0e545a17

Conversation

@JReinhold

@JReinhold JReinhold commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Closes #

What I did

Added internal visibility metadata to open-service definitions so services can be hidden from listServices() and queries/commands can be hidden from describeService() without disabling runtime access or typings.

Also added TypeScript-only _ prefix enforcement for internal operations and updated open-service registration/type tests plus maintainer docs.

Alternatively, we could also skip the internal flag completely and rely solely on the operation name. So if the query/command starts with _, it is implicitly hidden from descriptions. For services as a whole, if their id is prefixed _ the same could happen. Open to both solutions.

Example

import { defineService } from 'storybook/internal/open-service';
import { registerService } from 'storybook/internal/core-server';

export const exampleServiceDef = defineService({
  id: 'example/widget',
  description: 'Public widget service.',
  initialState: { count: 0 },
  queries: {
    getCount: {
      input: v.undefined(),
      output: v.number(),
      handler: (_input, ctx) => ctx.self.state.count,
    },
    _getDebugCount: {
      internal: true, // hidden from describeService(); must use _ prefix
      input: v.undefined(),
      output: v.number(),
      handler: (_input, ctx) => ctx.self.state.count,
    },
  },
  commands: {},
});

// Optional: hide the whole service from listServices()
export const internalDiagnosticsDef = defineService({
  id: 'example/diagnostics',
  internal: true,
  initialState: {},
  queries: {},
  commands: {},
});

const service = registerService(exampleServiceDef);

// Runtime still exposes internal operations
service.queries._getDebugCount(undefined);

// Discovery APIs omit internal metadata
await listServices(); // does not include internalDiagnosticsDef
await describeService('example/widget'); // only lists getCount, not _getDebugCount

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

No manual test is necessary. Maintainers can verify with:

  1. Run yarn test open-service

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Declare whether manual QA will be needed for this PR during the next release, through qa:needed or qa:skip

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • New Features

    • Added internal visibility controls allowing services and operations to be hidden from discovery APIs while remaining callable at runtime.
    • Internal operations must use _ prefix naming convention.
    • Static snapshot generation works with internal operations.
  • Documentation

    • Added documentation covering discovery visibility behavior and internal operation naming rules.
  • Tests

    • Added service definition fixtures and type validation tests for internal operation naming.

JReinhold and others added 2 commits June 4, 2026 21:47
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@JReinhold JReinhold marked this pull request as ready for review June 4, 2026 19:57
@JReinhold JReinhold requested a review from ndelangen June 4, 2026 19:57
@JReinhold JReinhold added the ci:normal Run our default set of CI jobs (choose this for most PRs). label Jun 4, 2026
@JReinhold JReinhold self-assigned this Jun 4, 2026
@JReinhold JReinhold added maintenance User-facing maintenance tasks core qa:skip Pull Requests that do not need any QA. labels Jun 4, 2026
@JReinhold JReinhold changed the title Add internal open-service visibility Open Service: Add internal property to control visibility Jun 4, 2026
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ac478a33-96a9-4920-b03e-550616f5f6ed

📥 Commits

Reviewing files that changed from the base of the PR and between 641036a and fb56c4e.

📒 Files selected for processing (7)
  • code/core/src/shared/open-service/README.md
  • code/core/src/shared/open-service/fixtures.ts
  • code/core/src/shared/open-service/index.test-d.ts
  • code/core/src/shared/open-service/service-definition.ts
  • code/core/src/shared/open-service/service-registration.test.ts
  • code/core/src/shared/open-service/service-registry.ts
  • code/core/src/shared/open-service/types.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • code/core/src/shared/open-service/types.ts
  • code/core/src/shared/open-service/service-definition.ts
  • code/core/src/shared/open-service/fixtures.ts
  • code/core/src/shared/open-service/index.test-d.ts

📝 Walkthrough

Walkthrough

This PR adds internal visibility control to the open-service framework. Services and individual operations can be marked internal: true to hide them from discovery APIs (describeService, listServices), while remaining callable at runtime and included in static builds. Internal operations require _ name prefixes, enforced bidirectionally at compile time via defineService().

Changes

Internal visibility and naming enforcement

Layer / File(s) Summary
Type-level contracts for internal visibility
code/core/src/shared/open-service/types.ts
QueryDefinition, CommandDefinition, and ServiceDefinition each gain optional internal?: boolean. Supporting record types AnyQueryDefinition and AnyCommandDefinition also receive the flag.
Compile-time naming validation for internal operations
code/core/src/shared/open-service/service-definition.ts
Type helpers enforce that operations marked internal: true must use _ prefix and vice versa. Integration into DefinedQueries and DefinedCommands applies the constraint during authoring. defineService() input extended to accept optional internal?: boolean.
Compile-time naming rule tests
code/core/src/shared/open-service/index.test-d.ts
TypeScript tests verify internal operations with _ prefix are accepted, internal without _ are rejected, and _ without internal: true are rejected.
Runtime discovery filtering implementation
code/core/src/shared/open-service/service-registry.ts
describeDefinition() filters out internal queries and commands from serialized metadata. listServices() filters out internal services from summaries. getService() and runtime invocation unaffected.
Test fixtures for visibility and static-build scenarios
code/core/src/shared/open-service/fixtures.ts
Six reusable service fixtures: unimplemented operations, command overrides at registration, static-build registration, mixed visibility (public and internal ops), fully hidden service, and internal static-build participation.
Test suite migration and behavioral verification
code/core/src/shared/open-service/service-registration.test.ts
Migrate registration tests to use fixtures. Add tests verifying discovery filtering behavior, service hiding, and static snapshot inclusion of internal operations.
Documentation of internal visibility feature
code/core/src/shared/open-service/README.md
Document discovery visibility mechanics and internal operation naming rules.

🎯 3 (Moderate) | ⏱️ ~25 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
code/core/src/shared/open-service/service-registration.test.ts (1)

228-230: ⚡ Quick win

Strengthen _reset runtime-access assertion with an actual state transition.

This currently passes even if _reset is a no-op, because state is already 0. Set a non-zero value first, then assert _reset brings it back to 0.

Suggested test adjustment
-    expect(service.queries._getInternalValue(undefined)).toBe(0);
-    await service.commands._reset(undefined);
-    expect(service.queries.getValue(undefined)).toBe(0);
+    await service.commands.setValue(5);
+    expect(service.queries._getInternalValue(undefined)).toBe(5);
+    await service.commands._reset(undefined);
+    expect(service.queries.getValue(undefined)).toBe(0);

Based on learnings, tests should validate observable side effects rather than relying on implementation-neutral initial state.

🤖 Prompt for 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.

In `@code/core/src/shared/open-service/service-registration.test.ts` around lines
228 - 230, The test currently verifies _reset by observing state already 0, so
it would pass if _reset is a no-op; modify the test to first change the service
state to a non-zero value using an available command (e.g., call the public
command that mutates state such as service.commands.increment or
service.commands.setValue — whichever exists in this service) and assert the
internal value is non-zero via service.queries._getInternalValue (or getValue)
before invoking service.commands._reset(undefined); then assert
service.queries.getValue(undefined) (and _getInternalValue) equals 0 after
_reset to ensure a real state transition occurred.
🤖 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.

Nitpick comments:
In `@code/core/src/shared/open-service/service-registration.test.ts`:
- Around line 228-230: The test currently verifies _reset by observing state
already 0, so it would pass if _reset is a no-op; modify the test to first
change the service state to a non-zero value using an available command (e.g.,
call the public command that mutates state such as service.commands.increment or
service.commands.setValue — whichever exists in this service) and assert the
internal value is non-zero via service.queries._getInternalValue (or getValue)
before invoking service.commands._reset(undefined); then assert
service.queries.getValue(undefined) (and _getInternalValue) equals 0 after
_reset to ensure a real state transition occurred.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f844b2f0-05db-46dc-a2c4-02888f17f71a

📥 Commits

Reviewing files that changed from the base of the PR and between 64ab797 and 121eacb.

📒 Files selected for processing (7)
  • code/core/src/shared/open-service/README.md
  • code/core/src/shared/open-service/fixtures.ts
  • code/core/src/shared/open-service/index.test-d.ts
  • code/core/src/shared/open-service/service-definition.ts
  • code/core/src/shared/open-service/service-registration.test.ts
  • code/core/src/shared/open-service/service-registration.ts
  • code/core/src/shared/open-service/types.ts

@storybook-app-bot

storybook-app-bot Bot commented Jun 4, 2026

Copy link
Copy Markdown

Package Benchmarks

Commit: fb56c4e, ran on 8 June 2026 at 12:46:51 UTC

No significant changes detected, all good. 👏

@AriPerkkio AriPerkkio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd say keep both internal and underscore prefix

  • Using internal as property is good for dropping such services/queries from runtime code. Checking for property is faster than doing name.startsWith("_")
  • Enforcing _ prefix gives clear intention of the query. Personally I don't care if it's underscore or what, as long as there is something that implies its intention. Too bad # isn't allowed outside classes.

I would even go as far as drop internal queries from typings of services that are discoverable by user.

@JReinhold

Copy link
Copy Markdown
Contributor Author

Checking for property is faster than doing name.startsWith("_")

This probably won't have any practical impact unless we have 20K services.

I would even go as far as drop internal queries from typings of services that are discoverable by user.

I had the same thought initially, but I feared it would be to cumbersome for the code that would actually consume these private operations. They would have to do type casts, which I would fear could hide issues from TS if they ever drift.

JReinhold and others added 2 commits June 5, 2026 14:48
Use explicit state casts so registration handlers can assign string values to nullable state fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve open-service conflicts by porting internal visibility filtering
into service-registry.ts after next replaced service-registration.ts.

Co-authored-by: Cursor <cursoragent@cursor.com>
@JReinhold JReinhold merged commit 2949311 into next Jun 8, 2026
144 checks passed
@JReinhold JReinhold deleted the jeppe-cursor/0e545a17 branch June 8, 2026 14:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:normal Run our default set of CI jobs (choose this for most PRs). core maintenance User-facing maintenance tasks qa:skip Pull Requests that do not need any QA.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants