Repository navigation
Angular: Generate static template snippets on the server #35796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
valentinpalkovic
wants to merge
4
commits into
valentin/csf-tools-story-shape-helpers
from
valentin/angular-story-docs-1-template-snippets
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
c8b1834
Core: Share the CSF story-shape helpers snippet generators need
valentinpalkovic 5d18b0d
Angular: Generate static template snippets on the server
valentinpalkovic 3341953
Apply suggestions from code review
valentinpalkovic c509c8a
Angular: Correct the metadata source in the story-docs limitations
valentinpalkovic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import type { types as t } from 'storybook/internal/babel'; | ||
| import { type NodePath, recast } from 'storybook/internal/babel'; | ||
|
|
||
| import { dedent } from 'ts-dedent'; | ||
|
|
||
| import { loadCsf } from '../CsfFile.ts'; | ||
| import { resolveRenderFunction } from './render.ts'; | ||
| import { normalizeStoryDeclaration } from './normalize-story.ts'; | ||
|
|
||
| /** Resolves `render` on story `A`, the way a snippet generator would. */ | ||
| const resolveStoryRender = (code: string) => { | ||
| const source = `export default { title: 'T' };\n${dedent(code)}`; | ||
| const csf = loadCsf(source, { makeTitle: (title) => title ?? 'title' }).parse(); | ||
| const declaration = csf._storyDeclarationPath['A']; | ||
| const normalized = normalizeStoryDeclaration(declaration); | ||
| const properties: NodePath<t.ObjectProperty>[] = | ||
| normalized.type === 'config' | ||
| ? normalized.path.get('properties').filter((p) => p.isObjectProperty()) | ||
| : []; | ||
|
|
||
| return resolveRenderFunction(properties, declaration); | ||
| }; | ||
|
|
||
| const printedBody = (resolution: ReturnType<typeof resolveStoryRender>) => | ||
| resolution.kind === 'resolved' ? recast.print(resolution.path.node).code : undefined; | ||
|
|
||
| describe('resolveRenderFunction', () => { | ||
| it('reports a story with no render property as missing', () => { | ||
| expect(resolveStoryRender(`export const A = { args: {} };`)).toEqual({ kind: 'missing' }); | ||
| }); | ||
|
|
||
| it('resolves an inline arrow function', () => { | ||
| expect(printedBody(resolveStoryRender(`export const A = { render: () => 1 };`))).toBe( | ||
| '() => 1' | ||
| ); | ||
| }); | ||
|
|
||
| it('follows an identifier to a local arrow function', () => { | ||
| expect( | ||
| printedBody( | ||
| resolveStoryRender(` | ||
| const Template = () => 1; | ||
| export const A = { render: Template }; | ||
| `) | ||
| ) | ||
| ).toBe('() => 1'); | ||
| }); | ||
|
|
||
| it('follows an identifier to a local function declaration', () => { | ||
| expect( | ||
| printedBody( | ||
| resolveStoryRender(` | ||
| function Template() { return 1; } | ||
| export const A = { render: Template }; | ||
| `) | ||
| ) | ||
| ).toBe('function Template() { return 1; }'); | ||
| }); | ||
|
|
||
| // The distinction that matters: an unreadable render is not the same as no render, because a | ||
| // caller may only fall back to the meta's render in the second case. | ||
| it('reports an identifier it cannot follow as unresolved rather than missing', () => { | ||
| expect(resolveStoryRender(`export const A = { render: ImportedTemplate };`)).toEqual({ | ||
| kind: 'unresolved', | ||
| }); | ||
| }); | ||
|
|
||
| it('reports an identifier bound to a non-function as unresolved', () => { | ||
| expect( | ||
| resolveStoryRender(` | ||
| const Template = 'not a function'; | ||
| export const A = { render: Template }; | ||
| `) | ||
| ).toEqual({ kind: 'unresolved' }); | ||
| }); | ||
|
|
||
| it('throws when render is present but is not a function at all', () => { | ||
| expect(() => resolveStoryRender(`export const A = { render: { nested: true } };`)).toThrow( | ||
| /Expected render to be an arrow function or function expression/ | ||
| ); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import type { types as t } from 'storybook/internal/babel'; | ||
| import { type NodePath } from 'storybook/internal/babel'; | ||
|
|
||
| import { keyOf, resolveIdentifierInit } from './utils.ts'; | ||
|
|
||
| /** A function a story or meta supplies through `render`. */ | ||
| export type RenderFunctionPath = NodePath< | ||
| t.ArrowFunctionExpression | t.FunctionExpression | t.FunctionDeclaration | ||
| >; | ||
|
|
||
| /** | ||
| * Outcome of looking for a `render` function. | ||
| * | ||
| * `missing` and `unresolved` have to stay distinct. A story whose `render` exists but cannot be | ||
| * read must not fall back to the meta's `render`: the story's intent was to override it, and | ||
| * quietly rendering the meta's version instead produces a snippet for code the story never runs. | ||
| */ | ||
| export type RenderResolution = | ||
| | { kind: 'missing' } | ||
| | { kind: 'resolved'; path: RenderFunctionPath } | ||
| | { kind: 'unresolved' }; | ||
|
|
||
| const isRenderFunction = (path: NodePath<t.Node>): path is RenderFunctionPath => | ||
| path.isArrowFunctionExpression() || path.isFunctionExpression() || path.isFunctionDeclaration(); | ||
|
|
||
| /** | ||
| * Resolves the `render` property of a story or meta config, following a local identifier | ||
| * (`render: Template`) to the function it names. | ||
| * | ||
| * `storyDeclaration` anchors the identifier lookup to the module the story lives in, so a helper | ||
| * declared beside the story resolves while an imported one reports `unresolved`. | ||
| * | ||
| * Throws when `render` is present but is neither a function nor an identifier, because that is a | ||
| * story-file mistake rather than something a static pass merely could not follow. | ||
| */ | ||
| export function resolveRenderFunction( | ||
| properties: NodePath<t.ObjectProperty>[], | ||
| storyDeclaration: NodePath<t.Node> | ||
| ): RenderResolution { | ||
| const renderPath = properties.find((property) => keyOf(property.node) === 'render')?.get('value'); | ||
|
|
||
| if (!renderPath) { | ||
| return { kind: 'missing' }; | ||
| } | ||
|
|
||
| if (renderPath.isIdentifier()) { | ||
| const resolved = resolveIdentifierInit(storyDeclaration, renderPath); | ||
| return resolved && isRenderFunction(resolved) | ||
| ? { kind: 'resolved', path: resolved } | ||
| : { kind: 'unresolved' }; | ||
| } | ||
|
|
||
| if (!isRenderFunction(renderPath)) { | ||
| throw renderPath.buildCodeFrameError( | ||
| 'Expected render to be an arrow function or function expression' | ||
| ); | ||
| } | ||
|
|
||
| return { kind: 'resolved', path: renderPath }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
code/frameworks/angular-vite/src/client/docs/config.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| // The flag is read once at module evaluation, so each case needs a fresh module registry. | ||
| const loadDecorators = async () => { | ||
| vi.resetModules(); | ||
| return (await import('./config.ts')).decorators; | ||
| }; | ||
|
|
||
| describe('angular docs decorators', () => { | ||
| beforeEach(() => { | ||
| vi.resetModules(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it('registers the source decorator by default', async () => { | ||
| await expect(loadDecorators()).resolves.toHaveLength(1); | ||
| }); | ||
|
|
||
| it('drops the source decorator when the server-side docs path is on', async () => { | ||
| vi.stubGlobal('FEATURES', { experimentalDocgenServer: true }); | ||
|
|
||
| await expect(loadDecorators()).resolves.toEqual([]); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
code/frameworks/angular-vite/src/docgen/__testfixtures__/documentation.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "components": [ | ||
| { | ||
| "name": "ButtonComponent", | ||
| "type": "component", | ||
| "file": "button.component.ts", | ||
| "selector": "sb-button", | ||
| "propertiesClass": [], | ||
| "methodsClass": [], | ||
| "inputsClass": [ | ||
| { "name": "label", "type": "string", "optional": false, "defaultValue": "'Badge'" }, | ||
| { "name": "count", "type": "number", "optional": true } | ||
| ], | ||
| "outputsClass": [{ "name": "clicked", "type": "EventEmitter<string>", "optional": false }] | ||
| } | ||
| ], | ||
| "directives": [], | ||
| "classes": [], | ||
| "miscellaneous": {} | ||
| } |
4 changes: 4 additions & 0 deletions
4
code/frameworks/angular-vite/src/docgen/__testfixtures__/no-component.stories.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| // Fixture: a story file with no `meta.component`, which the story-docs provider passes through. | ||
| export default { title: 'NoComponent' }; | ||
|
|
||
| export const Only = { args: { label: 'x' } }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the default feature-flag state explicit.
At Line 18, the test imports
config.tswithout settingFEATURES. A Vitest setup withexperimentalDocgenServer: truewould make this test load an empty decorator list instead of the default list. StubFEATURESwithexperimentalDocgenServer: falsebefore loading the module.As per coding guidelines, use
vi.stubGlobaland 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
🤖 Prompt for AI Agents
Source: Coding guidelines