Skip to content

Angular: resolve story-docs snippets through the core/docgen service instead of a second analyzer - #35843

Merged
valentinpalkovic merged 5 commits into
storybookjs:valentin/angular-story-docs-snippetsfrom
valentinpalkovic:valentin/angular-story-docs-osa-service
Aug 11, 2026
Merged

valentinpalkovic merged 5 commits into
storybookjs:valentin/angular-story-docs-snippetsfrom
valentinpalkovic:valentin/angular-story-docs-osa-service

Conversation

@valentinpalkovic

@valentinpalkovic valentinpalkovic commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Closes #

What I did

Alternative to #35841 for the same problem: story-docs built its own AngularComponentMetaManager right next to the one docgen-worker.ts already owns, so we ran two warm TypeScript language services over the same files, and story-docs's copy never called startWatching(), so its snippets went stale after a component edit while docgen kept updating live.

#35841 fixes that by widening the docgen worker protocol with a third message kind so story-docs can query the worker directly. This PR takes a different angle: core/docgen is already a registered OSA service, and story-docs already runs in the same main-thread process that service lives in. So instead of reaching into the worker, story-docs-preset.ts just resolves core/docgen as an internal service dependency:

 story render requested
        │
        ▼
core/story-docs query ──▶ experimental_storyDocsProvider (angular-vite, main thread)
        │
        │ getService('core/docgen', { internal: true })
        ▼
core/docgen query 'docgen' ──▶ .loaded({ id }) ──▶ extractDocgen command
        │                                                  │
        │ (already registered, already cached                docgenProvider(input)
        │  once docgen has run for this id)                   │
        │                                                     ▼
        │                                          docgenWorker.extract(entry)
        │                                          (angular-vite's worker-hosted
        │                                           analyzer - the ONE
        │                                           AngularComponentMetaManager)
        ▼
AngularDocgenPayload { argTypes, ..., angularComponentMeta: { entry, enums } }
        │
        ▼
story-docs reads entry (selector, inputsClass/outputsClass)
       + enums (for resolving Enum.Member args) straight off it

build-docgen.ts already attached the analyzer's class record to the stored docgen payload as angularComponentMeta. It now carries { entry, enums } rather than the bare class record: enums is the file's enum declarations, needed to resolve args like kind: ButtonKind.Secondary (enums aren't a property of the class - the analyzer collects them once per file, as a sibling of the class record, not nested inside it). That's deliberately just the one slice of the analyzer's file-level output story-docs reads, not the whole file metadata (which also lists every other component/directive/pipe the analyzer found in the file) - no point duplicating data already stored elsewhere for those. entry and enums are bundled into one field rather than two independent ones because they're always set or read together; two siblings would only rely on the two call sites agreeing by convention. core/docgen's schema is a v.looseObject, so the extra field passes through validation into service state untouched.

// story-docs-preset.ts, before
const manager = await (managerPromise ??= createManager());
const ours = buildStoryDocsPayload(input, { manager });

// after
const getDocgenPayload = async (componentId: string) => {
  const docgenService = getService('core/docgen', { internal: true });
  return docgenService.queries.docgen.loaded({ id: componentId });
};
const ours = await buildStoryDocsPayload(input, { getDocgenPayload });

Because the lookup is now keyed by component id (matching how core/docgen stores its state) rather than component path plus export/local name, buildStoryDocsPayload derives the id the same way docgen does (getComponentIdFromEntry) and no longer needs a manager-shaped dependency at all.

One extraction serves both consumers: querying story-docs for a component transparently triggers (or reuses the cached result of) the exact same docgenWorker.extract() call docgen would have made anyway. No second manager, no protocol change, and common-preset.ts is untouched, since getService resolves against the process-global registry at query time rather than needing anything threaded through preset composition.

Where this differs from #35841 (worth comparing before picking one)

  • Footprint: this PR touches 7 files, all inside angular-cm / angular-vite / docgen-harness. Angular: share one component-meta analyzer between docgen and story-docs #35841 touches those plus 7 files in code/core (protocol, worker, client, preset wiring). If we want the smallest diff, this wins.
  • Coupling: this PR piggybacks Angular-specific analyzer data onto core/docgen's cross-framework payload schema (a looseObject, so it's already loosely typed, but it does mean core/docgen's state now carries two fields no other framework's docgen provider populates). Angular: share one component-meta analyzer between docgen and story-docs #35841 keeps that data on a purpose-built, explicitly "framework-specific and opaque to core" worker message instead, so the generic docgen service stays framework-agnostic.
  • Freshness: both fixes share the same underlying manager and get the same startWatching() live-reload fix, since both ultimately route through the one docgenWorker.extract() call.
  • Failure mode: here, a story-docs snippet silently depends on core/docgen having successfully extracted for the same component; if docgen's own extraction failed for an unrelated reason, story-docs loses its snippet too (falls back the same way it already does when meta is undefined, so behavior isn't broken, just the failure surface is now shared). Angular: share one component-meta analyzer between docgen and story-docs #35841 keeps the two failure paths independent.

I don't have a strong preference yet, happy to close whichever we don't go with once we've talked it through.

What a bad run looks like

Dropping the enum table in createSnippetContext in story-docs-build.ts (passing [] instead of the real enums off angularComponentMeta, simulating "forgot to carry the enum table through") breaks the harness parity gate on the one fixture that actually exercises enum resolution:

$ yarn test --project "@storybook/docgen-harness" angular-story-docs-snippets

 FAIL  |@storybook/docgen-harness| src/angular/angular-story-docs-snippets.test.ts > angular story-docs server snippets > decorator-union-enum
Error: Snapshot `angular story-docs server snippets > decorator-union-enum 1` mismatched

Expected: "<sb-decorator-union-enum [size]="'large'" [tone]="'warn'" [kind]="'secondary'"></sb-decorator-union-enum>"
Received: "<sb-decorator-union-enum [size]="'large'" [tone]="'warn'" [kind]="ButtonKind.Secondary"></sb-decorator-union-enum>"

  Snapshots  1 failed
 Test Files  1 failed (1)
      Tests  1 failed | 10 passed (11)

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

  1. Check out this branch and run yarn && yarn nx run-many -t compile.
  2. Run yarn task sandbox --template angular-vite/docgen-server-ts --start-from auto (the sandbox with experimentalDocgenServer and componentsManifest on). Open a story's Docs page - argTypes and the Source block snippet should render exactly as they did before this PR.
  3. Without restarting the dev server, edit the story's component (e.g. add an @Input()). Both the argTypes table and the Source block snippet should pick up the change on the next load.
  4. yarn test docgen-worker story-docs-build angular-story-docs-snippets build-story-docs common-preset docgen should all pass.

Documentation

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

Internal worker plumbing behind an experimental flag, so nothing user-facing to document here.

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>

…instead of a second analyzer

Alternative to the shared-manager approach: instead of widening the docgen
worker protocol, story-docs-preset.ts now resolves the already-registered
core/docgen OSA service (getService('core/docgen', { internal: true })) and
reads the raw analyzer fields (selector, inputsClass/outputsClass, the enum
table) that build-docgen.ts now also stores on AngularDocgenPayload alongside
argTypes. No second AngularComponentMetaManager, no protocol change, no
common-preset.ts wiring: querying core/docgen for a component id transparently
triggers the same docgenWorker.extract() docgen already runs.
@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor
Fails
🚫

PR is not labeled with one of: ["cleanup","BREAKING CHANGE","feature request","bug","documentation","maintenance","build","dependencies"]

🚫

PR is not labeled with one of: ["ci:normal","ci:merged","ci:daily","ci:docs"]

🚫

PR is not labeled with one of: ["qa:needed","qa:skip","qa:success"]

🚫 PR title must be in the format of "Area: Summary", With both Area and Summary starting with a capital letter Good examples: - "Docs: Describe Canvas Doc Block" - "Svelte: Support Svelte v4" Bad examples: - "add new api docs" - "fix: Svelte 4 support" - "Vue: improve docs"
🚫 This PR needs an approving review from a Storybook Core or Developer Experience team member before it can be merged.
Warnings
⚠️

This PR targets valentin/angular-story-docs-snippets. The default branch for contributions is next. Please make sure you are targeting the correct branch.

Generated by 🚫 dangerJS against ec12d5c

@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aec4fc6c-ea9e-43ee-9435-0e57ff13ee07

📥 Commits

Reviewing files that changed from the base of the PR and between abb8de5 and ec12d5c.

📒 Files selected for processing (2)
  • code/frameworks/angular-vite/src/docgen/build-docgen.ts
  • code/frameworks/angular-vite/src/docgen/story-docs-build.ts

Walkthrough

Angular story-docs generation now retrieves metadata asynchronously through core/docgen. Docgen payloads include Angular enum declarations. Story-docs builders and tests use asynchronous payload callbacks with fallback handling for unavailable or failed queries.

Changes

Angular story-docs integration

Layer / File(s) Summary
Docgen payload metadata contract
code/frameworks/angular-vite/src/docgen/build-docgen.ts, code/frameworks/angular-vite/src/docgen/build-docgen.test.ts, code/frameworks/angular-vite/src/docgen/build-docgen.integration.test.ts
AngularDocgenPayload now contains an analyzer entry and file-level enum declarations. Successful payloads populate the enum list or use an empty list. Tests read the nested entry.
Async story-docs service flow
code/frameworks/angular-vite/src/docgen/story-docs-build.ts, code/frameworks/angular-vite/src/docgen/story-docs-preset.ts
Story-docs construction awaits getDocgenPayload. The preset queries core/docgen, handles failures, logs warnings once, and passes metadata and enums to snippet generation.
Story-docs integration validation
code/frameworks/angular-vite/src/docgen/story-docs-build.test.ts, code/lib/docgen-harness/src/angular/*
Tests use asynchronous payload callbacks and cover successful snippets, missing docgen data, unresolved components, and query failures.

Sequence Diagram(s)

sequenceDiagram
  participant StoryDocsProvider
  participant CoreDocgen
  participant BuildStoryDocsPayload
  StoryDocsProvider->>CoreDocgen: query component docgen payload
  CoreDocgen-->>StoryDocsProvider: AngularDocgenPayload or undefined
  StoryDocsProvider->>BuildStoryDocsPayload: await payload construction
  BuildStoryDocsPayload->>BuildStoryDocsPayload: create snippet context from entry and enums
Loading

Possibly related PRs


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

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

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 `@code/frameworks/angular-vite/src/docgen/story-docs-build.test.ts`:
- Around line 86-99: Replace direct getDocgenPayload resolver implementations
with a shared resolver spy declared before tests, and configure each mock
behavior in beforeEach using vi.mocked(). In
code/frameworks/angular-vite/src/docgen/story-docs-build.test.ts#L86-L99,
configure the successful payload; at `#L114-L116`, configure the rejected query;
in
code/lib/docgen-harness/src/angular/angular-story-docs-snippets.test.ts#L37-L63
and
code/lib/docgen-harness/src/angular/story-docs/build-story-docs.test.ts#L26-L52,
use the shared typed spy instead of direct resolver callbacks.

In `@code/frameworks/angular-vite/src/docgen/story-docs-preset.ts`:
- Around line 9-11: Update the comment near the `core/docgen` registration to
retain the rationale for defensively handling registration order and avoiding a
type assertion, but remove the explicit `common-preset.ts` cross-file reference.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a0520050-71ee-42a9-aba7-acc42b3ac264

📥 Commits

Reviewing files that changed from the base of the PR and between 18ef96c and 772acce.

📒 Files selected for processing (7)
  • code/frameworks/angular-vite/src/docgen/build-docgen.ts
  • code/frameworks/angular-vite/src/docgen/story-docs-build.test.ts
  • code/frameworks/angular-vite/src/docgen/story-docs-build.ts
  • code/frameworks/angular-vite/src/docgen/story-docs-preset.ts
  • code/lib/angular-cm/src/index.ts
  • code/lib/docgen-harness/src/angular/angular-story-docs-snippets.test.ts
  • code/lib/docgen-harness/src/angular/story-docs/build-story-docs.test.ts

Comment on lines +86 to +99
const getDocgenPayload = async (): Promise<AngularDocgenPayload> =>
({
id: 'example-button',
name: 'ButtonComponent',
path: STORY_PATH,
jsDocTags: {},
angularComponentMeta: {
name: 'ButtonComponent',
selector: 'sb-button',
inputsClass: [{ name: 'label' }],
outputsClass: [],
},
angularComponentMetaJson: { miscellaneous: { typealiases: [], enumerations: [] } },
}) as AngularDocgenPayload;

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the required Vitest spy setup for getDocgenPayload.

Declare the resolver spy before the test cases. Configure its behavior in beforeEach. Use vi.mocked() to access the typed mock.

  • code/frameworks/angular-vite/src/docgen/story-docs-build.test.ts#L86-L99: Configure the successful payload response in beforeEach.
  • code/frameworks/angular-vite/src/docgen/story-docs-build.test.ts#L114-L116: Configure the rejected query response in beforeEach.
  • code/lib/docgen-harness/src/angular/angular-story-docs-snippets.test.ts#L37-L63: Replace the direct resolver callback with the shared typed spy.
  • code/lib/docgen-harness/src/angular/story-docs/build-story-docs.test.ts#L26-L52: Replace the direct resolver callback with the shared typed spy.

As per coding guidelines, implement mock behaviors in beforeEach blocks, use vi.mocked() to type and access mocked functions, and avoid direct function mocking without vi.mocked().

📍 Affects 3 files
  • code/frameworks/angular-vite/src/docgen/story-docs-build.test.ts#L86-L99 (this comment)
  • code/frameworks/angular-vite/src/docgen/story-docs-build.test.ts#L114-L116
  • code/lib/docgen-harness/src/angular/angular-story-docs-snippets.test.ts#L37-L63
  • code/lib/docgen-harness/src/angular/story-docs/build-story-docs.test.ts#L26-L52
🤖 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/frameworks/angular-vite/src/docgen/story-docs-build.test.ts` around
lines 86 - 99, Replace direct getDocgenPayload resolver implementations with a
shared resolver spy declared before tests, and configure each mock behavior in
beforeEach using vi.mocked(). In
code/frameworks/angular-vite/src/docgen/story-docs-build.test.ts#L86-L99,
configure the successful payload; at `#L114-L116`, configure the rejected query;
in
code/lib/docgen-harness/src/angular/angular-story-docs-snippets.test.ts#L37-L63
and
code/lib/docgen-harness/src/angular/story-docs/build-story-docs.test.ts#L26-L52,
use the shared typed spy instead of direct resolver callbacks.

Source: Coding guidelines

Comment on lines +9 to +11
// `core/docgen` is only registered when `experimentalDocgenServer` set up its worker (see
// `common-preset.ts`); both services are gated by the same feature, but registration order isn't
// a type-level guarantee, so this stays defensive rather than asserting the service exists.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the cross-file source reference.

Keep the registration-order rationale. Remove the common-preset.ts reference from this comment.

As per coding guidelines, comments should explain maintenance-relevant rationale, not cross-file line references.

🤖 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/frameworks/angular-vite/src/docgen/story-docs-preset.ts` around lines 9
- 11, Update the comment near the `core/docgen` registration to retain the
rationale for defensively handling registration order and avoiding a type
assertion, but remove the explicit `common-preset.ts` cross-file reference.

Source: Coding guidelines

…yzer MetadataJson

angularComponentMetaJson mirrored the analyzer's own {entry, json} split
mechanically instead of asking what story-docs actually reads off json: only
miscellaneous.enumerations, to resolve Enum.Member story args. The rest of
MetadataJson (every other component/directive/pipe/class in the file) never
gets touched, so storing all of it on core/docgen's service state duplicated
data already present elsewhere.

Replaces angularComponentMetaJson?: MetadataJson with
angularComponentEnums?: EnumType[], the one slice that's needed. No longer
need to export MetadataJson from @storybook/angular-cm's public API either,
since angular-vite already imports EnumType from @storybook/angular-compodoc
directly.
@valentinpalkovic

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit addressing feedback on the first version of this: angularComponentMetaJson?: MetadataJson mechanically mirrored the analyzer's own { entry, json } split onto the docgen payload, but story-docs-build.ts only ever reads one thing off json: miscellaneous.enumerations, to resolve Enum.Member story args. The rest of MetadataJson (every other component/directive/pipe/class the analyzer found in the file) never gets touched by story-docs, so carrying the whole thing on core/docgen's service state duplicated data that already exists elsewhere for those other classes.

Replaced it with angularComponentEnums?: EnumType[], the one slice that's actually needed. Also means @storybook/angular-cm no longer needs to export MetadataJson from its public API at all, since angular-vite already imports EnumType from @storybook/angular-compodoc directly.

angularComponentMeta and angularComponentEnums were two independently-optional
sibling fields that only ever get set or read together, relying on the two
call sites to keep them in sync by convention rather than the type enforcing
it. Collapses them into one field, angularComponentMeta: { entry, enums },
still narrower than the analyzer's own AngularComponentMetaResult (drops the
unused rest of MetadataJson) but a single unit instead of two.
@valentinpalkovic

Copy link
Copy Markdown
Contributor Author

Pushed a second follow-up: collapsed `angularComponentMeta`/`angularComponentEnums` into one field, `angularComponentMeta: { entry, enums }`.

The two were always set and read together, but nothing in the type said so, only the two call sites agreeing by convention. Storing the full analyzer `AngularComponentMetaResult` (`{ entry, json }`) as a single field would also solve that, but reopens the original problem: `json` carries every other component/directive/pipe the analyzer found in the file, none of which story-docs touches. This keeps the narrowing (only `enums`, not the whole `json`) while making "these travel together" structural instead of conventional.

Comment thread code/frameworks/angular-vite/src/docgen/build-docgen.ts Outdated
Comment thread code/frameworks/angular-vite/src/docgen/story-docs-build.ts Outdated
@valentinpalkovic
valentinpalkovic merged commit 436a709 into storybookjs:valentin/angular-story-docs-snippets Aug 11, 2026
8 of 9 checks passed
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.

1 participant