Skip to content

Angular: Report what a snippet could not resolve - #35798

Draft
valentinpalkovic wants to merge 8 commits into
valentin/angular-story-docs-2-story-shapesfrom
valentin/angular-story-docs-3-warning
Draft

Angular: Report what a snippet could not resolve#35798
valentinpalkovic wants to merge 8 commits into
valentin/angular-story-docs-2-story-shapesfrom
valentin/angular-story-docs-3-warning

Conversation

@valentinpalkovic

@valentinpalkovic valentinpalkovic commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Note

Third of four Angular story-docs slices, stacked on #35797 (which now sits on #35807 -> #35808 -> #35805). First producer of the warning field added in #35794.

What I did

Angular story snippets are built on the server by reading the story file, not by running it. Some things cannot be read that way: args merged in through a spread, a template held in an imported const, a render that points at a function declared elsewhere. The snippet that comes out is still useful, but nothing marked it as partial, so a consumer had no way to tell an incomplete example from a complete one.

Every story left incomplete now carries a warning naming the source text this pass could not read, next to the snippet it produced anyway.

                       +--- template / render ---+
 story file --parse--> +--- meta config ---------+--> readable in full?
                       +--- story config --------+           |
                                                     yes ----+---- no
                                                      |             |
                                                      v             v
                                       snippet: markup     snippet: generated bindings
                                       as written          warning: the source text it
                                       (no warning)                 could not read

What changes in the output

The same story file, built before and after this change:

const sharedArgs = { label: 'shared' };
export default { title: 'Example/Button', component: ButtonComponent, args: { label: 'meta' } };
export const SpreadArgs = { args: { ...sharedArgs, count: 1 } };
export const Plain = { args: { count: 2 } };

Before, the two stories are indistinguishable:

{
  "example-button--spread-args": {
    "id": "example-button--spread-args",
    "name": "Spread Args",
    "snippet": "<sb-button [label]=\"'meta'\" [count]=\"1\" (clicked)=\"clicked($event)\"></sb-button>"
  },
  "example-button--plain": {
    "id": "example-button--plain",
    "name": "Plain",
    "snippet": "<sb-button [label]=\"'meta'\" [count]=\"2\" (clicked)=\"clicked($event)\"></sb-button>"
  }
}

After, the partial one says so and the complete one is left alone:

{
  "example-button--spread-args": {
    "id": "example-button--spread-args",
    "name": "Spread Args",
    "snippet": "<sb-button [label]=\"'meta'\" [count]=\"1\" (clicked)=\"clicked($event)\"></sb-button>",
    "warning": "Incomplete snippet: `...sharedArgs` could not be resolved statically."
  },
  "example-button--plain": {
    "id": "example-button--plain",
    "name": "Plain",
    "snippet": "<sb-button [label]=\"'meta'\" [count]=\"2\" (clicked)=\"clicked($event)\"></sb-button>"
  }
}

label in the first snippet is the meta's value, not sharedArgs's. The snippet itself is byte-identical in both runs; only the note next to it is new.

Why a field and not a comment in the snippet

The obvious alternative is to write it into the markup:

<!-- unresolved: ...sharedArgs -->
<sb-button [label]="'meta'" [count]="1"></sb-button>

That pushes the problem onto every consumer. The Docs Source block renders it as a stray comment, an agent reading the manifest has to parse it back out, and a story whose snippet is not HTML has no comment syntax to hide it in.

warning always comes with a snippet. error continues to mean there is no snippet at all.

What counts as unresolvable

A spread at the config level hides args just as invisibly as one inside args, so both are checked, along with computed keys and object methods:

const unresolvableProperties = (config: t.ObjectExpression | undefined): string[] =>
  [config, objectExpressionOf(propertyOf(config, 'args'))].flatMap((object) =>
    (object?.properties ?? [])
      .filter((property) => !t.isObjectProperty(property) || keyOf(property) === undefined)
      .map(sourceOf)
  );

This runs over the meta's config and the story's own, so a spread written once in the meta is reported on every story it leaves incomplete.

A template or render that could not be read as markup is reported the same way, which is what stops the silent fallback introduced in #35797 from being silent. Markup is looked for in three places in order (the story's own template/render, the object a CSF2 function story returns, then the meta's), and only one of them produces the result, so the warning names that one: it always points at the markup that would have been used.

One deliberate exception: a component with no selector renders as <ng-container *ngComponentOutlet="X">, which shows no args at all, so naming the args that could not be read would say nothing about what is missing from it. Those snippets carry no warning.

Command What it does Run from
yarn vitest run --project "@storybook/angular-vite" code/frameworks/angular-vite/src/docgen/story-docs-build.test.ts 34 tests covering spreads at both levels, imported templates and renders, and an unreadable interpolation repo root
yarn vitest run code/core/src/core-server/utils/manifests 38 tests, including that the field survives into the merged component manifest repo root

What a bad run looks like

With the producer removed and the tests kept, yarn vitest run --project "@storybook/angular-vite" code/frameworks/angular-vite/src/docgen/story-docs-build.test.ts prints:

 × names the markup the Imported Template story fell back from 3ms
 × names the markup the Imported Render story fell back from 2ms
 × names the markup the Unreadable Interpolation story fell back from 2ms
 × reports args a spread hid rather than shipping a snippet that looks complete 3ms
 × reports a spread at the config level, not only one inside args 2ms
 × reports a spread in the meta on the stories it leaves incomplete 4ms

AssertionError: expected undefined to be 'Incomplete snippet: `...sharedArgs` c…' // Object.is equality
AssertionError: expected undefined to be 'Incomplete snippet: `...SpreadArgs` c…' // Object.is equality
AssertionError: expected undefined to be 'Incomplete snippet: `...shared` could…' // Object.is equality

 Test Files  1 failed (1)
      Tests  6 failed | 28 passed (34)

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/docgen-server-ts --start-from auto. That template is the Angular sandbox with experimentalDocgenServer and componentsManifest already on.
  2. In the sandbox, add a story whose args come from a spread:
    const shared = { label: 'Hi' };
    export const Spread = { args: { ...shared, primary: true } };
  3. Build the sandbox (yarn build-storybook).
  4. Open storybook-static/manifests/components.json, find the component, and follow its stories $ref into storybook-static/services/core/story-docs/<component-id>.json.
  5. The Spread story carries a warning naming ...shared, and its snippet contains no HTML comment. The other stories in the same file carry no warning.

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.

@github-actions

github-actions Bot commented Aug 7, 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"]

Warnings
⚠️

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

Generated by 🚫 dangerJS against cc76565

@valentinpalkovic
valentinpalkovic force-pushed the valentin/angular-story-docs-3-warning branch from 8ab3bd3 to ce7d9ad Compare August 7, 2026 16:40
@valentinpalkovic
valentinpalkovic force-pushed the valentin/angular-story-docs-2-story-shapes branch from 056d892 to a431b81 Compare August 7, 2026 16:43
@valentinpalkovic
valentinpalkovic force-pushed the valentin/angular-story-docs-3-warning branch from ce7d9ad to c0429f9 Compare August 7, 2026 16:43
@valentinpalkovic
valentinpalkovic force-pushed the valentin/angular-story-docs-2-story-shapes branch from a431b81 to 23fdf78 Compare August 7, 2026 16:49
@valentinpalkovic
valentinpalkovic force-pushed the valentin/angular-story-docs-3-warning branch 3 times, most recently from ce9df5d to d9dbeea Compare August 7, 2026 17:09
@valentinpalkovic
valentinpalkovic force-pushed the valentin/angular-story-docs-2-story-shapes branch from 740865a to e46f5c9 Compare August 8, 2026 05:27
Server-side snippets are built by reading the story file, not by running it, so
some things cannot be resolved: args merged in through a spread, a template held
in an imported const, a `render` that points at a function declared elsewhere.

The snippet that comes out is still useful, but until now nothing said it was
partial. A story that fell back now carries a `warning` naming the source text
this pass could not read, alongside the snippet it produced anyway.

The note is data on the story rather than a comment inside the markup, so a
consumer that renders the snippet is not left with a stray comment and one that
reads it can act on it. `warning` always comes with a `snippet`; `error`
continues to mean there is no snippet at all.
@valentinpalkovic
valentinpalkovic force-pushed the valentin/angular-story-docs-3-warning branch from f3061fa to 3d41670 Compare August 8, 2026 05:42
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