Skip to content

Angular: Generate static template snippets on the server - #35796

Closed
valentinpalkovic wants to merge 4 commits into
valentin/csf-tools-story-shape-helpersfrom
valentin/angular-story-docs-1-template-snippets
Closed

valentinpalkovic wants to merge 4 commits into
valentin/csf-tools-story-shape-helpersfrom
valentin/angular-story-docs-1-template-snippets

Conversation

@valentinpalkovic

@valentinpalkovic valentinpalkovic commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Note

First of four Angular story-docs slices. This one is the whole vertical: a story file goes in, a snippet comes out of the Docs Source block. Later slices add story shapes beyond plain args (#2), incompleteness reporting (#3), and a full-component snippet format (#4).

What I did

Today an Angular story's Source block is filled in by the browser: Storybook loads the component class, reads its inputs and outputs through Angular's reflection APIs, and builds the template string in the preview. That only works with a compiler present, so nothing outside the browser can produce a snippet - the CLI and MCP can't, and neither can anything that wants the docs without rendering the story.

This builds the same kind of snippet in Node, behind experimentalDocgenServer. Property and event bindings only. With the flag off nothing changes at all.

 button.stories.ts ──parse CSF──┐
                                ├──► [label]="'Save'"  ──┐
 AngularComponentResolver ──────┤    (clicked)="…"       ├──► <sb-button …></sb-button>
     selector, inputs, outputs ─┤                        │
                                │    host element ───────┘
   ▲                            │    from the selector
   └─ @storybook/angular-cm     │

How this is measured

Fifteen snippets are recorded across eleven Angular fixtures and compared against the ones the browser path produces, so no binding may go missing. The recorded output for a two-way model(), which stays an input plus a Change output rather than banana-in-a-box:

<!-- browser -->
<sb-signal-model [value]="'hello'" [checked]="true" (valueChange)="valueChange($event)" (checkedChange)="checkedChange($event)"></sb-signal-model>
<!-- server -->
<sb-signal-model [checked]="true" [value]="'hello'" (checkedChange)="checkedChange($event)" (valueChange)="valueChange($event)"></sb-signal-model>

Attribute order and formatting are free to differ; a lost binding is not. Dropping the outputs a story never declared looks like this:

Error: expectCurrentOrBetter found 1 violation(s):
- [lost-representation] clicked: represented in the baseline snippet but not in the candidate
Command What it does Run from
yarn vitest run code/lib/docgen-harness/src/angular Records the server snippets and compares them to the browser baselines repo root
yarn vitest run code/frameworks/angular-vite/src/docgen Unit tests for selector parsing, value serialization, the resolver contract and the provider repo root
yarn vitest run code/lib/docgen-harness/src/angular -u Re-records after an intentional change; review the snapshot diff, it is the only place a value regression shows up repo root

Known limitations, all documented next to the generator

Structural directives, banana-in-a-box and content projection are out of scope. Values are read from the source rather than evaluated, so args: { kind: ButtonKind.Secondary } prints as written.

The one accepted regression: the snippet is static, so it no longer follows the Controls. That was agreed for the server-side docs path and already applies to Vue.

A story that supplies its own template, a CSF2 function story, or a re-exported story is not read yet and falls back to generated bindings. That is the next slice, and no fixture in the parity harness exercises those shapes, which is why they split cleanly.

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. yarn task sandbox --template angular-vite/default-ts --start-from auto
  2. In the sandbox's .storybook/main.ts, set features: { experimentalDocgenServer: true }
  3. yarn storybook
  4. Open Example/Button → Docs and press Show code. You should see a real Angular template, not the story's source:
    <storybook-button [label]="'Button'" [primary]="true" (onClick)="onClick($event)"></storybook-button>
  5. Toggle the primary control to False. The story re-renders, the snippet does not change - that is the static-snippet trade-off, and it is also how you can tell the server generator is the one driving the block.
  6. Turn the flag back off and confirm the Source block behaves exactly as it does on next.

Warning

With experimentalDocgenServer on, Compodoc does not run at all, so a sandbox where Compodoc fails no longer affects the path this PR adds. Step 6, which turns the flag back off, does still hit it: Compodoc 2 crashes on TypeScript 7, and a freshly generated Angular sandbox now resolves TypeScript 7 because the template's typescript@^6 pin sits in dependencies while the Angular CLI scaffold's ^7.0.2 sits in devDependencies. Pinning typescript back to ^6 in the sandbox works around it. This breaks the existing browser path too, so it is not caused by this PR.

Documentation

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

The v1 limitations are written up in the framework package as source material for the user-facing docs page, which a later story owns.

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.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor
Fails
🚫

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

🚫 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/csf-tools-story-shape-helpers. The default branch for contributions is next. Please make sure you are targeting the correct branch.

Generated by 🚫 dangerJS against c509c8a

Comment thread code/frameworks/angular-vite/src/docgen/template-snippet.ts
Comment thread code/frameworks/angular-vite/src/docgen/story-docs-preset.ts Outdated
Comment thread code/frameworks/angular-vite/src/docgen/compodoc-component-resolver.ts Outdated
Comment thread code/frameworks/angular-vite/src/docgen/build-story-docs.ts Outdated
@valentinpalkovic
valentinpalkovic marked this pull request as ready for review August 7, 2026 12:15
@storybook-app-bot

storybook-app-bot Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Package Benchmarks

Commit: c509c8a, ran on 7 August 2026 at 18:46:15 UTC

No significant changes detected, all good. 👏

@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

🧹 Nitpick comments (7)
code/frameworks/angular-vite/src/docgen/build-story-docs.ts (1)

61-70: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider caching the parsed CSF file per story path.

buildStoryDocsPayload runs once per index entry. Each call reads and Babel-parses the whole story file again. A component with many stories parses the same file repeatedly on the request path. A small per-path cache keyed on the resolved storyPath removes that repeated work.

🤖 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/build-story-docs.ts` around lines 61
- 70, Cache the parsed CSF result used by buildStoryDocsPayload, keyed by the
resolved storyPath, so repeated index entries reuse the same parse instead of
rereading and reparsing the file. Update the existing story-loading flow around
loadCsf and preserve its current debug logging and undefined return behavior
when parsing fails.
code/frameworks/angular-vite/src/docgen/compodoc-component-resolver.ts (1)

44-50: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Contain extractArgTypesFromData failures like the other two failure paths.

The resolver already degrades to undefined when readMetadata throws and when findCompodocEntry finds nothing. extractArgTypesFromData runs unguarded. If it throws on malformed Compodoc metadata, the error propagates through buildStoryDocsPayload into the provider handler in story-docs-preset.ts, which has no catch, so the story-docs request rejects instead of falling back to the next provider.

♻️ Proposed guard
-    const argTypes = extractArgTypesFromData(entry, {
-      compodocJson,
-      // Snippets bind inputs and outputs, which this flag never filters.
-      filterNonInputControls: false,
-      logger: options.logger,
-      unwrapHtml: htmlToText,
-    });
+    let argTypes;
+    try {
+      argTypes = extractArgTypesFromData(entry, {
+        compodocJson,
+        // Snippets bind inputs and outputs, which this flag never filters.
+        filterNonInputControls: false,
+        logger: options.logger,
+        unwrapHtml: htmlToText,
+      });
+    } catch (error) {
+      options.logger.debug(
+        `Could not extract Compodoc arg types for "${component.exportName}": ${error instanceof Error ? error.message : String(error)}.`
+      );
+      return undefined;
+    }
🤖 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/compodoc-component-resolver.ts`
around lines 44 - 50, Wrap the extractArgTypesFromData call in the resolver’s
existing failure-handling pattern so exceptions from malformed Compodoc metadata
are caught and the resolver returns undefined, matching the readMetadata and
missing-entry paths. Keep the current extraction options unchanged and ensure
the error is handled locally before buildStoryDocsPayload propagates it.
code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts (2)

16-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the historical precedent from this comment.

State only why these snapshots remain the baseline. The vue3 precedent does not provide a maintenance instruction.

As per coding guidelines, comments must explain maintenance-relevant rationale and not investigation history.

🤖 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/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts` around
lines 16 - 18, Update the comment above the recorder setup to remove the
reference to the vue3 precedent and historical investigation context. State only
that the committed snippet-*.snapshot files remain the baseline and
BASELINE_PATH is intentionally left untouched.

Source: Coding guidelines


5-7: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Use memfs for these filesystem tests.

Both tests read repository fixture files through real node:fs APIs. This makes the tests depend on the checkout state and bypasses the required Vitest spy-mocking pattern. Mock node:fs with spy: true at the top of each test file. Reset and seed vol in beforeEach. Redirect required vi.mocked() filesystem spies to memfs.

  • code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts#L5-L7: redirect readFileSync and readdirSync to seeded memfs data for every Angular fixture.
  • code/frameworks/angular-vite/src/docgen/build-story-docs.test.ts#L3-L5: redirect readFileSync to seeded story and Compodoc fixture data.

As per coding guidelines, filesystem tests using node:fs must use memfs, reset vol in beforeEach, and use Vitest spy redirection rather than real filesystem access.

🤖 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/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts` around
lines 5 - 7, Update
code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts lines 5-7 and
code/frameworks/angular-vite/src/docgen/build-story-docs.test.ts lines 3-5 to
mock node:fs with spy: true, reset and seed memfs vol in each file’s beforeEach,
and redirect the required vi.mocked() filesystem spies to the seeded fixtures.
In angular-osa-snippets.test.ts redirect readFileSync and readdirSync for
Angular fixtures; in build-story-docs.test.ts redirect readFileSync for story
and Compodoc fixtures, eliminating real filesystem access.

Source: Coding guidelines

code/lib/docgen-harness/src/angular/angular-provider-seam.test.ts (1)

35-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace this syntax-only assertion with a registration behavior test.

typeof experimental_storyDocsProvider === 'function' only verifies the direct module export. TypeScript and the import already cover that contract. Test preset resolution or provider invocation through the public registration path instead.

As per coding guidelines, “Test public contracts and externally observable side effects rather than private implementation details” and “avoid syntax-only tests.”

🤖 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/lib/docgen-harness/src/angular/angular-provider-seam.test.ts` around
lines 35 - 37, Replace the syntax-only assertion in the angular-vite
registration test with a behavioral test through the public registration path,
such as resolving the preset or invoking the registered provider. Assert the
externally observable provider registration result rather than the typeof value
of experimental_storyDocsProvider.

Source: Coding guidelines

code/frameworks/angular-vite/src/docgen/story-docs-preset.test.ts (2)

26-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move test-double behavior into typed Vitest mocks.

Lines 30-34, 47, 69, and 78-84 define mock behavior outside beforeEach or inline in test cases. Use shared typed spies, access them through vi.mocked(), and configure their resolved values in beforeEach.

As per coding guidelines, “Use vi.mocked() to type and access the mocked functions in Vitest tests,” “Implement mock behaviors in beforeEach blocks,” and “Avoid inline mock implementations within test cases in Vitest tests.”

Also applies to: 47-47, 69-69, 77-86

🤖 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.test.ts` around
lines 26 - 36, Refactor the test doubles in the story-docs preset tests,
including the options helper and the mocks at the referenced test cases, to use
shared typed Vitest spies accessed through vi.mocked(). Move all mock
implementations and resolved-value configuration into beforeEach, leaving test
bodies to invoke the already-configured spies without inline behavior.

Source: Coding guidelines


50-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the acceptance-criteria code from this comment.

NFR3 is not maintenance rationale. Keep the explanation of the Node execution requirement without the internal code.

As per coding guidelines, “Comments should explain maintenance-relevant rationale, not investigation history, internal ticket or acceptance-criteria codes, provenance claims, or 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.test.ts` around
lines 50 - 51, Update the comment above the relevant test to remove the “NFR3”
acceptance-criteria label and the internal assertion/provenance wording, while
retaining only the maintenance-relevant explanation that the preset must be
callable as a plain Node function.

Source: Coding guidelines

🤖 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/client/docs/config.test.ts`:
- Around line 18-20: Update the `registers the source decorator by default` test
to stub the global `FEATURES` with `experimentalDocgenServer: false` via
`vi.stubGlobal` before loading `config.ts` or calling `loadDecorators()`, and
restore the ambient global state after the test using the existing Vitest
cleanup mechanism.

In
`@code/frameworks/angular-vite/src/docgen/__testfixtures__/story-docs.stories.ts`:
- Line 3: Update the ButtonComponent import in the story fixture to include the
explicit .ts extension, changing the module specifier from ./button.component to
./button.component.ts.

---

Nitpick comments:
In `@code/frameworks/angular-vite/src/docgen/build-story-docs.ts`:
- Around line 61-70: Cache the parsed CSF result used by buildStoryDocsPayload,
keyed by the resolved storyPath, so repeated index entries reuse the same parse
instead of rereading and reparsing the file. Update the existing story-loading
flow around loadCsf and preserve its current debug logging and undefined return
behavior when parsing fails.

In `@code/frameworks/angular-vite/src/docgen/compodoc-component-resolver.ts`:
- Around line 44-50: Wrap the extractArgTypesFromData call in the resolver’s
existing failure-handling pattern so exceptions from malformed Compodoc metadata
are caught and the resolver returns undefined, matching the readMetadata and
missing-entry paths. Keep the current extraction options unchanged and ensure
the error is handled locally before buildStoryDocsPayload propagates it.

In `@code/frameworks/angular-vite/src/docgen/story-docs-preset.test.ts`:
- Around line 26-36: Refactor the test doubles in the story-docs preset tests,
including the options helper and the mocks at the referenced test cases, to use
shared typed Vitest spies accessed through vi.mocked(). Move all mock
implementations and resolved-value configuration into beforeEach, leaving test
bodies to invoke the already-configured spies without inline behavior.
- Around line 50-51: Update the comment above the relevant test to remove the
“NFR3” acceptance-criteria label and the internal assertion/provenance wording,
while retaining only the maintenance-relevant explanation that the preset must
be callable as a plain Node function.

In `@code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts`:
- Around line 16-18: Update the comment above the recorder setup to remove the
reference to the vue3 precedent and historical investigation context. State only
that the committed snippet-*.snapshot files remain the baseline and
BASELINE_PATH is intentionally left untouched.
- Around line 5-7: Update
code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts lines 5-7 and
code/frameworks/angular-vite/src/docgen/build-story-docs.test.ts lines 3-5 to
mock node:fs with spy: true, reset and seed memfs vol in each file’s beforeEach,
and redirect the required vi.mocked() filesystem spies to the seeded fixtures.
In angular-osa-snippets.test.ts redirect readFileSync and readdirSync for
Angular fixtures; in build-story-docs.test.ts redirect readFileSync for story
and Compodoc fixtures, eliminating real filesystem access.

In `@code/lib/docgen-harness/src/angular/angular-provider-seam.test.ts`:
- Around line 35-37: Replace the syntax-only assertion in the angular-vite
registration test with a behavioral test through the public registration path,
such as resolving the preset or invoking the registered provider. Assert the
externally observable provider registration result rather than the typeof value
of experimental_storyDocsProvider.
🪄 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: 9e63efcc-fd3f-474d-a656-abeccd20aa25

📥 Commits

Reviewing files that changed from the base of the PR and between ce31afc and 7901b02.

📒 Files selected for processing (37)
  • code/frameworks/angular-vite/src/client/docs/config.test.ts
  • code/frameworks/angular-vite/src/client/docs/config.ts
  • code/frameworks/angular-vite/src/docgen/__testfixtures__/documentation.json
  • code/frameworks/angular-vite/src/docgen/__testfixtures__/no-component.stories.ts
  • code/frameworks/angular-vite/src/docgen/__testfixtures__/story-docs.stories.ts
  • code/frameworks/angular-vite/src/docgen/build-docgen.ts
  • code/frameworks/angular-vite/src/docgen/build-story-docs.test.ts
  • code/frameworks/angular-vite/src/docgen/build-story-docs.ts
  • code/frameworks/angular-vite/src/docgen/compodoc-component-resolver.ts
  • code/frameworks/angular-vite/src/docgen/docgen-worker.ts
  • code/frameworks/angular-vite/src/docgen/documentation-json.ts
  • code/frameworks/angular-vite/src/docgen/logger.ts
  • code/frameworks/angular-vite/src/docgen/resolve-component.ts
  • code/frameworks/angular-vite/src/docgen/story-docs-limitations.md
  • code/frameworks/angular-vite/src/docgen/story-docs-preset.test.ts
  • code/frameworks/angular-vite/src/docgen/story-docs-preset.ts
  • code/frameworks/angular-vite/src/docgen/template-snippet.test.ts
  • code/frameworks/angular-vite/src/docgen/template-snippet.ts
  • code/frameworks/angular-vite/src/preset.ts
  • code/lib/angular-compodoc/src/compodoc-types.ts
  • code/lib/docgen-harness/src/angular/__testfixtures__/complex-selector/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/cross-file-inheritance/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-generic/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-getter-setter/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-io-basics/osa-snippet-EventHandlerArg.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-io-basics/osa-snippet-ExplicitUndefinedArg.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-io-basics/osa-snippet-ObjectAndArrayArgs.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-io-basics/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-union-enum/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/expression-defaults/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/jsdoc-tags/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/properties-methods-noise/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/signal-io/osa-snippet-EventHandlerArg.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/signal-io/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/signal-model/osa-snippet-TwoWayBinding.snapshot
  • code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts
  • code/lib/docgen-harness/src/angular/angular-provider-seam.test.ts

Comment on lines +18 to +20
it('registers the source decorator by default', async () => {
await expect(loadDecorators()).resolves.toHaveLength(1);
});

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the default feature-flag state explicit.

At Line 18, the test imports config.ts without setting FEATURES. A Vitest setup with experimentalDocgenServer: true would make this test load an empty decorator list instead of the default list. Stub FEATURES with experimentalDocgenServer: false before loading the module.

As per coding guidelines, use vi.stubGlobal and restore ambient globals in tests.

Suggested fix
   it('registers the source decorator by default', async () => {
+    vi.stubGlobal('FEATURES', { experimentalDocgenServer: false });
     await expect(loadDecorators()).resolves.toHaveLength(1);
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('registers the source decorator by default', async () => {
await expect(loadDecorators()).resolves.toHaveLength(1);
});
it('registers the source decorator by default', async () => {
vi.stubGlobal('FEATURES', { experimentalDocgenServer: false });
await expect(loadDecorators()).resolves.toHaveLength(1);
});
🤖 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/client/docs/config.test.ts` around lines 18
- 20, Update the `registers the source decorator by default` test to stub the
global `FEATURES` with `experimentalDocgenServer: false` via `vi.stubGlobal`
before loading `config.ts` or calling `loadDecorators()`, and restore the
ambient global state after the test using the existing Vitest cleanup mechanism.

Source: Coding guidelines

@@ -0,0 +1,20 @@
// Fixture: the story shapes the Angular story-docs provider has to tell apart.
// Excluded from the package tsconfig, so the loose typing here is deliberate.
import { ButtonComponent } from './button.component';

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -HI -t f 'tsconfig.*\.json$' . -x sh -c '
  echo "--- $1"
  rg -n "allowImportingTsExtensions|moduleResolution|noEmit" "$1" || true
' sh {}

rg -n -C 2 "button\.component" \
  code/frameworks/angular-vite/src/docgen/__testfixtures__/story-docs.stories.ts

Repository: storybookjs/storybook

Length of output: 27693


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- relevant fixture files'
fd -HI -t f . code/frameworks/angular-vite/src/docgen/__testfixtures__ | sort | sed -n '1,120p'

echo '--- fixture tsconfig files'
for f in code/frameworks/angular-vite/src/docgen/__testfixtures__/tsconfig.json \
         code/frameworks/angular-vite/tsconfig.json \
         code/tsconfig.json; do
  if [ -f "$f" ]; then
    echo "--- $f"
    cat -n "$f"
  fi
done

echo '--- package and workspace configuration references'
rg -n -C 3 \
  'story-docs\.stories|__testfixtures__|angular-vite/src/docgen|docgen.*fixture|allowImportingTsExtensions|verbatimModuleSyntax' \
  code/frameworks/angular-vite code/package.json package.json .storybook 2>/dev/null | sed -n '1,240p'

echo '--- import-extension patterns in neighboring TypeScript fixtures'
rg -n \
  "^import .* from ['\"]\./[^'\"]+['\"];" \
  code/frameworks/angular-vite/src/docgen/__testfixtures__ \
  code/lib/docgen-harness/src/angular/__testfixtures__ 2>/dev/null | sed -n '1,160p'

echo '--- referenced component path'
python3 - <<'PY'
from pathlib import Path
p = Path("code/frameworks/angular-vite/src/docgen/__testfixtures__/story-docs.stories.ts")
for spec in ["./button.component", "./button.component.ts"]:
    target = p.parent / spec.removeprefix("./")
    print(f"{spec}: {target} exists={target.exists()} is_file={target.is_file()}")
PY

Repository: storybookjs/storybook

Length of output: 29244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- fixture source files'
for f in \
  code/frameworks/angular-vite/src/docgen/__testfixtures__/story-docs.stories.ts \
  code/frameworks/angular-vite/src/docgen/__testfixtures__/button.stories.ts \
  code/frameworks/angular-vite/src/docgen/__testfixtures__/no-component.stories.ts \
  code/frameworks/angular-vite/src/docgen/build-story-docs.test.ts \
  code/frameworks/angular-vite/src/docgen/build-story-docs.ts; do
  if [ -f "$f" ]; then
    echo "--- $f"
    cat -n "$f" | sed -n '1,240p'
  fi
done

echo '--- implementation and loader references'
rg -n -C 4 \
  'loadCsf|jiti|tsx|typescript|resolve.*import|import.*path|storyPath|readFileSync|createRequire' \
  code/frameworks/angular-vite/src/docgen code/lib 2>/dev/null | \
  sed -n '1,320p'

echo '--- extensionless relative imports in the target fixture subtree'
rg -n \
  "^import .* from ['\"]\./[^'\"]+['\"];" \
  code/frameworks/angular-vite/src/docgen/__testfixtures__ || true

echo '--- extension-bearing relative imports in nearby Angular docgen tests'
rg -n \
  "^import .* from ['\"]\./[^'\"]+\.(ts|tsx|js|jsx)['\"];" \
  code/frameworks/angular-vite/src/docgen | sed -n '1,160p'

Repository: storybookjs/storybook

Length of output: 46008


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Angular component resolver'
cat -n code/frameworks/angular-vite/src/docgen/resolve-component.ts | sed -n '1,180p'

echo '--- resolver implementation and extension handling'
rg -n -C 5 \
  'createMetaComponentResolver|importId|extensions:|tsconfig: .auto.|resolve.*extension|\.component' \
  code/core code/lib code/frameworks 2>/dev/null | \
  sed -n '1,360p'

echo '--- exact resolver test expectations'
cat -n code/frameworks/angular-vite/src/docgen/resolve-component.test.ts | sed -n '115,145p'

echo '--- TypeScript extension policy configuration'
rg -n -C 4 \
  'explicit.*extension|import.*extension|allowImportingTsExtensions|noRelative.*Extension|extension' \
  code/.eslintrc* code/eslint* .eslintrc* eslint.config.* package.json code/package.json 2>/dev/null | \
  sed -n '1,240p'

Repository: storybookjs/storybook

Length of output: 37439


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- all tracked and installed definitions of createMetaComponentResolver'
rg -uu -n -C 8 \
  'createMetaComponentResolver' \
  code node_modules 2>/dev/null | sed -n '1,360p'

echo '--- resolver-related module resolution helpers'
rg -uu -n -C 6 \
  'resolveImport|resolve.*module|extensions.*js|extensions.*ts|\.tsx.*\.jsx|tsconfig.*auto' \
  code/core code/lib node_modules/storybook 2>/dev/null | sed -n '1,360p'

Repository: storybookjs/storybook

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

log=/tmp/coderabbit-shell-logs/shell-output-0vCwg3

echo '--- relevant match locations'
rg -n -m 30 'createMetaComponentResolver|resolveImport|extensions.*(js|ts)|tsconfig.*auto' "$log" || true

Repository: storybookjs/storybook

Length of output: 3741


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n code/core/src/common/utils/resolve-meta-component.ts | sed -n '1,150p'
cat -n code/core/src/shared/constants/extensions.ts | sed -n '1,100p'

Repository: storybookjs/storybook

Length of output: 7318


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n code/core/src/common/utils/module-resolver.ts | sed -n '1,220p'

Repository: storybookjs/storybook

Length of output: 1939


Add the .ts extension to the fixture import. Change ./button.component to ./button.component.ts.

🤖 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/__testfixtures__/story-docs.stories.ts`
at line 3, Update the ButtonComponent import in the story fixture to include the
explicit .ts extension, changing the module specifier from ./button.component to
./button.component.ts.

Source: Coding guidelines

Base automatically changed from valentin/story-docs-warning-field to next August 7, 2026 12:39
@valentinpalkovic
valentinpalkovic force-pushed the valentin/angular-story-docs-1-template-snippets branch from 7901b02 to 79d7bff Compare August 7, 2026 16:43
React's snippet generator grew several helpers that are not React-specific:
resolving a `render` identifier to the function it names, telling an absent
`render` apart from an unreadable one, and reading CSF2's `Story.args = {}`
assignment form. The Angular and Vue providers need the same answers.

They move into `csf-tools/story-shape` and React imports them back, so its
snapshots are what proves the move is behaviour-preserving. `propertyValue`
and `returnedObjectExpression` join them for the providers stacked on top.
valentinpalkovic and others added 2 commits August 7, 2026 18:44
An Angular story's Source block is filled in by the browser today: Storybook
loads the component class, reads its inputs and outputs through Angular's
reflection APIs, and builds the template in the preview. That needs a compiler,
so nothing outside the browser can produce a snippet.

Behind `experimentalDocgenServer`, this builds the same snippet in Node from the
component's declared selector and bindings plus the args written in the story
file. Property and event bindings only. With the flag off nothing changes.

Metadata reaches the generator through a resolver rather than being read from
Compodoc directly, so the engine behind it can be replaced without touching
snippet generation.
Co-authored-by: Valentin Palkovic <dev@valentinpalkovic.dev>
@coderabbitai

coderabbitai Bot commented Aug 7, 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: b71daeb2-451c-402f-9ebd-8701d5d1981f

📥 Commits

Reviewing files that changed from the base of the PR and between 79d7bff and c509c8a.

📒 Files selected for processing (5)
  • code/frameworks/angular-vite/src/docgen/build-docgen.ts
  • code/frameworks/angular-vite/src/docgen/docgen-worker.ts
  • code/frameworks/angular-vite/src/docgen/story-docs-limitations.md
  • code/frameworks/angular-vite/src/preset.ts
  • code/lib/angular-compodoc/src/compodoc-types.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • code/frameworks/angular-vite/src/docgen/story-docs-limitations.md
  • code/frameworks/angular-vite/src/preset.ts
  • code/lib/angular-compodoc/src/compodoc-types.ts
  • code/frameworks/angular-vite/src/docgen/build-docgen.ts
  • code/frameworks/angular-vite/src/docgen/docgen-worker.ts

Walkthrough

Added Angular server-side story documentation generation, Compodoc component resolution, static Angular snippet rendering, provider registration, and feature-flagged client decorator selection. Added fixtures, unit tests, OSA snapshots, provider seam coverage, and limitations documentation.

Changes

Angular static snippet generation

Layer / File(s) Summary
Template parsing and rendering
code/frameworks/angular-vite/src/docgen/template-snippet.ts, code/frameworks/angular-vite/src/docgen/template-snippet.test.ts
Angular selectors, expressions, inputs, outputs, model bindings, escaping, and selector-less components now produce static template snippets.
Snippet harness coverage
code/lib/docgen-harness/src/angular/*
Added Angular OSA snapshot fixtures and a harness that validates generated snippets against snapshots and legacy baselines.

Angular story documentation

Layer / File(s) Summary
Story payload construction
code/frameworks/angular-vite/src/docgen/build-story-docs.ts, code/frameworks/angular-vite/src/docgen/compodoc-component-resolver.ts, code/frameworks/angular-vite/src/docgen/build-docgen.ts, code/frameworks/angular-vite/src/docgen/resolve-component.ts, code/frameworks/angular-vite/src/docgen/__testfixtures__/*
Stories are parsed, Angular components are resolved from Compodoc or supplied resolver metadata, args are merged, snippets are generated, and story-level errors are preserved.
Payload validation
code/frameworks/angular-vite/src/docgen/build-story-docs.test.ts
Tests cover payload metadata, JSDoc, fallback cases, missing files and components, isolated story errors, and resolver independence from Compodoc.

Story-docs provider integration

Layer / File(s) Summary
Provider and metadata infrastructure
code/frameworks/angular-vite/src/docgen/story-docs-preset.ts, code/frameworks/angular-vite/src/docgen/documentation-json.ts, code/frameworks/angular-vite/src/docgen/logger.ts, code/frameworks/angular-vite/src/docgen/docgen-worker.ts
The experimental provider builds matching story payloads, reads cached Compodoc metadata, delegates fallback cases, and reuses shared logging and metadata utilities.
Provider registration and behavior tests
code/frameworks/angular-vite/src/docgen/story-docs-preset.test.ts, code/frameworks/angular-vite/src/preset.ts, code/lib/docgen-harness/src/angular/angular-provider-seam.test.ts
The preset exports the provider. Tests cover direct invocation, downstream merging, path failures, Compodoc opt-out, and provider registration.
Documented limitations
code/frameworks/angular-vite/src/docgen/story-docs-limitations.md
Documents supported bindings, value rendering, metadata requirements, unsupported story args, and fallback behavior.

Sequence Diagram(s)

sequenceDiagram
  participant StoryDocsRequest
  participant experimental_storyDocsProvider
  participant buildStoryDocsPayload
  participant createCompodocComponentResolver
  participant documentationJsonReader
  StoryDocsRequest->>experimental_storyDocsProvider: request story documentation
  experimental_storyDocsProvider->>buildStoryDocsPayload: build matching story payload
  buildStoryDocsPayload->>createCompodocComponentResolver: resolve Angular component
  createCompodocComponentResolver->>documentationJsonReader: read Compodoc metadata
  documentationJsonReader-->>createCompodocComponentResolver: return CompodocJson
  createCompodocComponentResolver-->>buildStoryDocsPayload: return component metadata
  buildStoryDocsPayload-->>experimental_storyDocsProvider: return generated payload
  experimental_storyDocsProvider-->>StoryDocsRequest: merge or delegate payload
Loading

Possibly related PRs

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

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: 3

♻️ Duplicate comments (1)
code/frameworks/angular-vite/src/client/docs/config.test.ts (1)

18-20: 🎯 Functional Correctness | 🟡 Minor

Make the default feature-flag state explicit.

If the Vitest setup defines globalThis.FEATURES.experimentalDocgenServer as true, this test expects one decorator but receives none. Stub FEATURES with { experimentalDocgenServer: false } before calling loadDecorators().

Suggested fix
   it('registers the source decorator by default', async () => {
+    vi.stubGlobal('FEATURES', { experimentalDocgenServer: false });
     await expect(loadDecorators()).resolves.toHaveLength(1);
   });

As per coding guidelines, use vi.stubGlobal for ambient globals and restore them with vi.unstubAllGlobals().

🤖 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/client/docs/config.test.ts` around lines 18
- 20, Update the “registers the source decorator by default” test to stub the
ambient FEATURES global with experimentalDocgenServer set to false before
calling loadDecorators(), and restore the stub with vi.unstubAllGlobals() after
the test.

Source: Coding guidelines

🧹 Nitpick comments (3)
code/frameworks/angular-vite/src/docgen/story-docs-preset.test.ts (1)

94-94: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Configure the process.cwd behavior in beforeEach.

Line 94 changes mock behavior inside the test case. Keep the mock implementation in beforeEach, then update a shared test value in this case.

Proposed fix
 const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), '__testfixtures__');
+let currentWorkingDirectory = FIXTURES;
 
 // `storyRoot` and `workspaceRoot` differ here on purpose.
 beforeEach(() => {
-  vi.spyOn(process, 'cwd').mockReturnValue(FIXTURES);
+  currentWorkingDirectory = FIXTURES;
+  vi.spyOn(process, 'cwd').mockImplementation(() => currentWorkingDirectory);
 });
@@
   it('falls through when the story path resolves against cwd but Compodoc lives elsewhere', async () => {
-    vi.mocked(process.cwd).mockReturnValue(join(FIXTURES, 'aliased'));
+    currentWorkingDirectory = join(FIXTURES, 'aliased');
     const provider = await experimental_storyDocsProvider(noDownstream, options());
🤖 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.test.ts` at line
94, Move the vi.mocked(process.cwd) configuration into the test suite’s
beforeEach setup, and in the affected test update the shared fixture value
instead of changing the mock behavior there. Preserve the test’s aliased fixture
behavior through that shared value.

Source: Coding guidelines

code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts (1)

5-7: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Use memfs for filesystem access in these tests.

Both tests read checked-out fixture files through node:fs. Seed the required files in vol during beforeEach, reset vol between cases, and redirect filesystem calls with Vitest spies.

  • code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts#L5-L7: Seed story, Compodoc, baseline, and OSA snapshot files in memfs. Read expected snapshots through the redirected filesystem path.
  • code/frameworks/angular-vite/src/docgen/build-story-docs.test.ts#L3-L5: Seed documentation.json and story fixtures in memfs before each test.

As per coding guidelines, “For filesystem tests involving node:fs or node:fs/promises, use memfs; reset vol in beforeEach [and] seed virtual files.”

🤖 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/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts` around
lines 5 - 7, Replace direct fixture filesystem access in
code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts (lines 5-7)
with memfs: reset vol in beforeEach, seed the story, Compodoc, baseline, and OSA
snapshot files, and redirect filesystem calls with Vitest spies so expected
snapshots are read virtually. Apply the same setup in
code/frameworks/angular-vite/src/docgen/build-story-docs.test.ts (lines 3-5),
resetting vol and seeding documentation.json and story fixtures before each
test.

Source: Coding guidelines

code/lib/docgen-harness/src/angular/angular-provider-seam.test.ts (1)

33-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or replace this syntax-only test.

typeof experimental_storyDocsProvider === 'function' only repeats module-export validation. It does not test preset registration or provider behavior. Test the provider through its public preset contract, or rely on story-docs-preset.test.ts for provider behavior.

As per coding guidelines, “Test public contracts and externally observable side effects rather than private implementation details” and “test real behavior and avoid syntax-only tests.”

🤖 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/lib/docgen-harness/src/angular/angular-provider-seam.test.ts` around
lines 33 - 37, Remove the syntax-only test for experimental_storyDocsProvider in
the angular-vite registration suite. Rely on story-docs-preset.test.ts for
provider behavior, or replace this assertion with coverage of the public preset
contract and an externally observable registration/provider effect.

Source: Coding guidelines

🤖 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/core/src/csf-tools/story-shape/args.ts`:
- Around line 55-74: Update the assignment search around the program traversal
to inspect only direct Program.body expression statements, excluding assignments
inside callbacks, functions, or other nested scopes. Preserve matching for
module-level storyName.args assignments, and add a regression test covering a
nested or shadowed A.args assignment so unrelated assignments cannot set found.

In `@code/frameworks/angular-vite/src/docgen/story-docs-preset.test.ts`:
- Around line 50-51: Remove the internal “NFR3” acceptance-criteria identifier
from the comment while preserving its explanation that the test is callable as a
plain Node function and validates the actual snippet output.

In `@code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts`:
- Around line 16-18: Update the comment above the recorder setup to remove the
“following the vue3 `cm-` precedent” provenance reference, while retaining the
explanation that committed `snippet-*.snapshot` files remain the measurement
oracle and `BASELINE_PATH` is intentionally untouched.

---

Duplicate comments:
In `@code/frameworks/angular-vite/src/client/docs/config.test.ts`:
- Around line 18-20: Update the “registers the source decorator by default” test
to stub the ambient FEATURES global with experimentalDocgenServer set to false
before calling loadDecorators(), and restore the stub with vi.unstubAllGlobals()
after the test.

---

Nitpick comments:
In `@code/frameworks/angular-vite/src/docgen/story-docs-preset.test.ts`:
- Line 94: Move the vi.mocked(process.cwd) configuration into the test suite’s
beforeEach setup, and in the affected test update the shared fixture value
instead of changing the mock behavior there. Preserve the test’s aliased fixture
behavior through that shared value.

In `@code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts`:
- Around line 5-7: Replace direct fixture filesystem access in
code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts (lines 5-7)
with memfs: reset vol in beforeEach, seed the story, Compodoc, baseline, and OSA
snapshot files, and redirect filesystem calls with Vitest spies so expected
snapshots are read virtually. Apply the same setup in
code/frameworks/angular-vite/src/docgen/build-story-docs.test.ts (lines 3-5),
resetting vol and seeding documentation.json and story fixtures before each
test.

In `@code/lib/docgen-harness/src/angular/angular-provider-seam.test.ts`:
- Around line 33-37: Remove the syntax-only test for
experimental_storyDocsProvider in the angular-vite registration suite. Rely on
story-docs-preset.test.ts for provider behavior, or replace this assertion with
coverage of the public preset contract and an externally observable
registration/provider effect.
🪄 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: 8e2f9c5f-7654-4eb4-bc33-095ef76c96e9

📥 Commits

Reviewing files that changed from the base of the PR and between 64f0b42 and 79d7bff.

📒 Files selected for processing (47)
  • code/core/src/core-server/utils/manifests/components-ref-manifest.test.ts
  • code/core/src/csf-tools/story-shape/args.test.ts
  • code/core/src/csf-tools/story-shape/args.ts
  • code/core/src/csf-tools/story-shape/index.ts
  • code/core/src/csf-tools/story-shape/render.test.ts
  • code/core/src/csf-tools/story-shape/render.ts
  • code/core/src/csf-tools/story-shape/utils.ts
  • code/core/src/shared/open-service/services/story-docs/definition.ts
  • code/core/src/shared/open-service/services/story-docs/types.ts
  • code/frameworks/angular-vite/src/client/docs/config.test.ts
  • code/frameworks/angular-vite/src/client/docs/config.ts
  • code/frameworks/angular-vite/src/docgen/__testfixtures__/documentation.json
  • code/frameworks/angular-vite/src/docgen/__testfixtures__/no-component.stories.ts
  • code/frameworks/angular-vite/src/docgen/__testfixtures__/story-docs.stories.ts
  • code/frameworks/angular-vite/src/docgen/build-docgen.ts
  • code/frameworks/angular-vite/src/docgen/build-story-docs.test.ts
  • code/frameworks/angular-vite/src/docgen/build-story-docs.ts
  • code/frameworks/angular-vite/src/docgen/compodoc-component-resolver.ts
  • code/frameworks/angular-vite/src/docgen/docgen-worker.ts
  • code/frameworks/angular-vite/src/docgen/documentation-json.ts
  • code/frameworks/angular-vite/src/docgen/logger.ts
  • code/frameworks/angular-vite/src/docgen/resolve-component.ts
  • code/frameworks/angular-vite/src/docgen/story-docs-limitations.md
  • code/frameworks/angular-vite/src/docgen/story-docs-preset.test.ts
  • code/frameworks/angular-vite/src/docgen/story-docs-preset.ts
  • code/frameworks/angular-vite/src/docgen/template-snippet.test.ts
  • code/frameworks/angular-vite/src/docgen/template-snippet.ts
  • code/frameworks/angular-vite/src/preset.ts
  • code/lib/angular-compodoc/src/compodoc-types.ts
  • code/lib/docgen-harness/src/angular/__testfixtures__/complex-selector/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/cross-file-inheritance/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-generic/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-getter-setter/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-io-basics/osa-snippet-EventHandlerArg.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-io-basics/osa-snippet-ExplicitUndefinedArg.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-io-basics/osa-snippet-ObjectAndArrayArgs.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-io-basics/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/decorator-union-enum/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/expression-defaults/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/jsdoc-tags/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/properties-methods-noise/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/signal-io/osa-snippet-EventHandlerArg.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/signal-io/osa-snippet-PropsAsWritten.snapshot
  • code/lib/docgen-harness/src/angular/__testfixtures__/signal-model/osa-snippet-TwoWayBinding.snapshot
  • code/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts
  • code/lib/docgen-harness/src/angular/angular-provider-seam.test.ts
  • code/renderers/react/src/componentManifest/generateCodeSnippet.ts

Comment on lines +55 to +74
program.traverse({
AssignmentExpression(assignment) {
const left = assignment.get('left');
const right = assignment.get('right');
if (!left.isMemberExpression() || !right.isObjectExpression()) {
return;
}

const object = left.get('object');
const property = left.get('property');
const isStory = object.isIdentifier() && object.node.name === storyName;
const isArgs =
(property.isIdentifier() && property.node.name === 'args' && !left.node.computed) ||
(t.isStringLiteral(property.node) && left.node.computed && property.node.value === 'args');

if (isStory && isArgs) {
found = right;
}
},
});

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restrict args assignments to module-level statements.

Line 55 traverses nested scopes. A callback parameter named A can assign A.args, and this function will use that unrelated assignment for story A.

Scan only direct Program.body expression statements. Add a regression test for a nested or shadowed A.args assignment. This prevents incorrect React and static-doc snippets.

Proposed fix
-  program.traverse({
-    AssignmentExpression(assignment) {
+  for (const statement of program.get('body')) {
+    if (!statement.isExpressionStatement()) {
+      continue;
+    }
+    const assignment = statement.get('expression');
+    if (!assignment.isAssignmentExpression()) {
+      continue;
+    }
       const left = assignment.get('left');
       const right = assignment.get('right');
       if (!left.isMemberExpression() || !right.isObjectExpression()) {
-        return;
+        continue;
       }
       // existing member checks
       if (isStory && isArgs) {
         found = right;
       }
-    },
-  });
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
program.traverse({
AssignmentExpression(assignment) {
const left = assignment.get('left');
const right = assignment.get('right');
if (!left.isMemberExpression() || !right.isObjectExpression()) {
return;
}
const object = left.get('object');
const property = left.get('property');
const isStory = object.isIdentifier() && object.node.name === storyName;
const isArgs =
(property.isIdentifier() && property.node.name === 'args' && !left.node.computed) ||
(t.isStringLiteral(property.node) && left.node.computed && property.node.value === 'args');
if (isStory && isArgs) {
found = right;
}
},
});
for (const statement of program.get('body')) {
if (!statement.isExpressionStatement()) {
continue;
}
const assignment = statement.get('expression');
if (!assignment.isAssignmentExpression()) {
continue;
}
const left = assignment.get('left');
const right = assignment.get('right');
if (!left.isMemberExpression() || !right.isObjectExpression()) {
continue;
}
const object = left.get('object');
const property = left.get('property');
const isStory = object.isIdentifier() && object.node.name === storyName;
const isArgs =
(property.isIdentifier() && property.node.name === 'args' && !left.node.computed) ||
(t.isStringLiteral(property.node) && left.node.computed && property.node.value === 'args');
if (isStory && isArgs) {
found = right;
}
}
🤖 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/csf-tools/story-shape/args.ts` around lines 55 - 74, Update the
assignment search around the program traversal to inspect only direct
Program.body expression statements, excluding assignments inside callbacks,
functions, or other nested scopes. Preserve matching for module-level
storyName.args assignments, and add a regression test covering a nested or
shadowed A.args assignment so unrelated assignments cannot set found.

Comment on lines +50 to +51
// NFR3: cold-callable as a plain Node function. Everything it needs comes from the preset
// options and the filesystem, and the assertion is the real snippet rather than "it returned".

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 acceptance-criteria code.

NFR3 is an internal acceptance-criteria code. Keep the test rationale without that identifier.

Proposed fix
-  // NFR3: cold-callable as a plain Node function. Everything it needs comes from the preset
+  // The provider is cold-callable as a plain Node function. Everything it needs comes from the preset
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// NFR3: cold-callable as a plain Node function. Everything it needs comes from the preset
// options and the filesystem, and the assertion is the real snippet rather than "it returned".
// The provider is cold-callable as a plain Node function. Everything it needs comes from the preset
// options and the filesystem, and the assertion is the real snippet rather than "it returned".
🤖 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.test.ts` around
lines 50 - 51, Remove the internal “NFR3” acceptance-criteria identifier from
the comment while preserving its explanation that the test is callable as a
plain Node function and validates the actual snippet output.

Source: Coding guidelines

Comment on lines +16 to +18
// A second recorder alongside the legacy one, following the vue3 `cm-` precedent: the committed
// `snippet-*.snapshot` files stay the oracle this path is measured against instead of being
// overwritten by it, so `BASELINE_PATH` is deliberately untouched.

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 precedent reference from this comment.

“following the vue3 cm- precedent” is a provenance claim. Keep only the rationale for preserving the committed baseline files.

Proposed fix
-// A second recorder alongside the legacy one, following the vue3 `cm-` precedent: the committed
+// A second recorder alongside the legacy one: the committed
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// A second recorder alongside the legacy one, following the vue3 `cm-` precedent: the committed
// `snippet-*.snapshot` files stay the oracle this path is measured against instead of being
// overwritten by it, so `BASELINE_PATH` is deliberately untouched.
// A second recorder alongside the legacy one: the committed
// `snippet-*.snapshot` files stay the oracle this path is measured against instead of being
// overwritten by it, so `BASELINE_PATH` is deliberately untouched.
🤖 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/lib/docgen-harness/src/angular/angular-osa-snippets.test.ts` around
lines 16 - 18, Update the comment above the recorder setup to remove the
“following the vue3 `cm-` precedent” provenance reference, while retaining the
explanation that committed `snippet-*.snapshot` files remain the measurement
oracle and `BASELINE_PATH` is intentionally untouched.

Source: Coding guidelines

@valentinpalkovic
valentinpalkovic force-pushed the valentin/angular-story-docs-1-template-snippets branch from 79d7bff to 3341953 Compare August 7, 2026 16:49
@valentinpalkovic
valentinpalkovic changed the base branch from next to valentin/csf-tools-story-shape-helpers August 7, 2026 16:49
@valentinpalkovic
valentinpalkovic force-pushed the valentin/csf-tools-story-shape-helpers branch 2 times, most recently from 69eca6c to c8b1834 Compare August 7, 2026 18:30
Under the experimental docgen flag the snippets read the in-process analyzer,
not Compodoc's documentation.json.
@valentinpalkovic
valentinpalkovic force-pushed the valentin/csf-tools-story-shape-helpers branch from c8b1834 to 7780a27 Compare August 7, 2026 19:59
@valentinpalkovic

Copy link
Copy Markdown
Contributor Author

Closing this in favour of #35805, which already ships the same vertical through the in-process analyzer rather than Compodoc.

Both branches ended up implementing server-side snippets independently. Scored against the browser snapshots the parity harness treats as truth, across the same 15 fixtures:

byte-identical to the browser baseline
#35805 13 / 15
this PR 6 / 15

The two places it shows:

<!-- browser -->
<sb-decorator-union-enum [size]="'large'" [tone]="'warn'" [kind]="'secondary'"></sb-decorator-union-enum>
<!-- #35805 -->
<sb-decorator-union-enum [size]="'large'" [tone]="'warn'" [kind]="'secondary'"></sb-decorator-union-enum>
<!-- this PR -->
<sb-decorator-union-enum [kind]="ButtonKind.Secondary" [size]="'large'" [tone]="'warn'"></sb-decorator-union-enum>

#35805 resolves the enum member to its value and keeps declaration order; this one leaves ButtonKind.Secondary as written and sorts the bindings.

The deciding factor is not the score, though. The resolver here reads Compodoc's documentation.json, and under experimentalDocgenServer Compodoc is no longer on that pipeline at all, so this implementation has no source to read from once #35805 lands.

Nothing is lost. The three slices stacked on top of this one are genuinely additive and are being re-pointed at #35805's story-docs-build.ts and story-docs-snippet.ts: story shapes that supply their own markup (#35797), incompleteness reporting (#35798), and the component snippet format (#35799).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

angular ci:normal Run our default set of CI jobs (choose this for most PRs). feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant