Skip to content

feat(babel-preset-storybook-full-source): add opt-in per-story fullSource slicing#36404

Draft
Hotell wants to merge 14 commits into
microsoft:masterfrom
Hotell:feat/babel-preset-per-story-fullsource
Draft

feat(babel-preset-storybook-full-source): add opt-in per-story fullSource slicing#36404
Hotell wants to merge 14 commits into
microsoft:masterfrom
Hotell:feat/babel-preset-per-story-fullsource

Conversation

@Hotell

@Hotell Hotell commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

@fluentui/babel-preset-storybook-full-source attaches context.parameters.fullSource to stories (consumed by the docs Show code panel and Open in Sandbox). Today it works per file: it reads the whole story file and attaches it to the last story export.

That fits the one-story-per-file convention, but not the standard CSF pattern of multiple story exports per file:

  • only the last export gets a fullSource; sibling stories get undefined;
  • CSF3 object stories (export const X = { render }) aren't attached in a form the sandbox can render.

This PR adds an opt-in per-story mode that gives every story its own minimal, renderable fullSource.

What's added

  • New option storyGranularity: 'file' | 'story' — defaults to 'file' (current behavior, unchanged).
  • 'story' mode — for each story export, emit its own fullSource containing only the imports and module-level helper declarations that story actually references, plus the story normalized to a renderable function:
    • CSF2 function stories kept as-is;
    • CSF3 object stories normalized to functions — { render }, args-only (rendered via the meta component), and spread ({ ...Base });
    • play / parameters omitted from the emitted sample string only (source files and runtime story objects are never modified — interaction tests are untouched);
    • reachability follows both value and type references (type aliases, interfaces, generics, indexed-access types), so types a story uses aren't pruned;
    • non-story capitalized exports (e.g. shared data objects) are ignored.
  • Formatting fidelity — the slicer mutates the original AST and prints with retainLines + preserved comments, so per-story output matches the authored source rather than a re-generated approximation.

Backward compatibility

  • Default 'file'no behavior change unless a consumer opts into 'story'.
  • Verified against this repo's full v9 story corpus (~1,095 story files):
    • 'file' mode output is byte-identical to master;
    • 'story' vs 'file': 0 coverage regressions, multi-story files gain proper per-story coverage, and single-story output is byte-identical or differs only by beneficial pruning of dead story configuration.

Testing

  • New fixtures: CSF2 multi-story, CSF3 multi-story with inline custom components, CSF3 render/args/spread, non-story data export, and TypeScript type preservation.
  • Unit tests, lint, and build are green; __fixtures__ excluded from tsconfig.lib/spec.
  • Beachball change file included (patch).

Notes / follow-ups

  • Making 'story' the default is intentionally deferred to a follow-up (needs a docs/output re-baseline).
  • Consumers using multiple stories per file opt in via the preset option; no changes are required in the sandbox addon or docs pipeline (they already read parameters.fullSource per story).

Hotell added 14 commits July 15, 2026 21:26
…urce slicing

Adds a 'storyGranularity: story' option that emits a per-story sliced fullSource for files with multiple story exports (standard CSF), instead of attaching the whole file to the last export.

- New sliceStory.ts: reachability-based slicer keeping only the imports/helpers a story references; normalizes CSF3 object stories (render, render-with-args, args-only synth via meta.component) and spread stories into renderable functions; omits play/parameters from the sample only.
- fullsource.ts collects all story exports and branches by granularity; 'file' mode unchanged.
- Guards args-only synth to genuine story-shaped objects so capitalized data exports are ignored.
- Fixtures + tests for CSF2 multi-story, CSF3 render/args/spread, and data-export cases.
…ing in per-story slices

Unify the slicer output with 'file' granularity so single-story slices are byte-identical to the whole-file path (no cosmetic churn):

- sliceStorySource now mutates the original AST in place and prints with retainLines + preserved comments, instead of regenerating via @babel/generator.
- CSF2 stories/helpers keep their original nodes (only stripping a story-type annotation) so authored formatting survives; only CSF3 transforms build new nodes.
- story-mode second pass uses retainLines to match file mode.
- Fix: reachability now follows type references (TSTypeReference/TSTypeQuery) and resolves local type/interface/enum declarations, so type aliases and their imports are no longer wrongly pruned (dangling type refs).
- Strip locations from synthesized CSF3 merged args for clean formatting.

Corpus effect: 916/939 single-story fullSource outputs become byte-identical to file mode; the rest are beneficial pruning (meta + dead argTypes/@storybook config).
…e; fix sibling comment leak

- Add csf3-multi-story fixture proving per-story reachability across multiple CSF3 object stories that pull in different subsets of shared imports/helpers (Primary -> Button+useStyles, Loading -> Spinner only, Group -> all).
- Fix: comments belonging to removed nodes (sibling stories, meta) no longer leak onto retained neighbours in a slice — track each comment's owning node and drop comments whose owner was pruned.
…n csf3-multi-story fixture

Exercises per-story reachability for function-form story exports in a CSF3-meta file with a module-scoped React component: Primary -> Button, Loading -> Spinner (meta.component Button not leaked), Group -> local ButtonRow.
…ine custom components

Multiple custom React components (Card/CardHeader/CardMedia/CardFooter) in the module; CSF3 object stories each use a subset. Verifies each story's fullSource preserves only the inline custom components it references (Basic -> Card+CardHeader, WithMedia -> Card+CardMedia, Full -> all four).
…ture to multi-story-csf3

Align naming with the sibling multi-story-csf2 fixture (same purpose, CSF3 variant).
…fixture for type preservation

Verifies per-story slicing preserves/prunes TypeScript types: type aliases, interfaces, type imports, generics (React.FC<T>) and indexed-access types (ButtonProps['appearance']). Simple keeps CardProps; WithAction keeps ActionProps + ButtonProps; neither leaks the other's types. Adds @babel/preset-typescript to the per-story test preset to parse the .tsx fixture.
…lib/spec tsconfigs

Fixture code/output files (now including .tsx) are test inputs, not source — exclude them from type-checking to avoid build failures.
…r in merged imports

- modifyImports now tracks type-only specifiers (import type / inline type) and re-emits them as a separate 'import type { … }' from the mapped package, instead of merging them into a value import (which breaks under verbatimModuleSyntax/isolatedModules).
- Per-story test parses TS via parserOpts (jsx + typescript) instead of @babel/preset-typescript, so the emitted module retains its types — demonstrating the preset is pass-through and does not strip types from source.
…in modifyImports

Add type-import-declaration fixture asserting that import type { X } and inline import { type X } from mapped packages are merged into a single 'import type { … }' (value and type imports kept separate). Refresh multi-story-typescript output.
… deterministic

The outer-compiled fixture section printed TS type-member separators non-deterministically (, vs ;) when parsing-without-stripping types. Compile the fixture with @babel/preset-typescript (types stripped in the module, as in a real Storybook build); type preservation remains asserted on the deterministic, prettier-normalized fullSource strings.
…type modifier change

Revert the modifyImports import-type preservation change and its fixture — out of scope for the per-story fullSource branch. The multi-story-typescript fixture stays (tests per-story TS type reachability) with the pre-existing merged-import behavior.
…ogic

Extract buildFullSource() (transform + prettier) and emitStorySource() (parameters + fullSource + cssModuleSources) helpers shared by both granularity branches. Collect CSS module imports once (file-level, was re-scanned per story). Unify the has-parameters check on storiesWithParameters and drop the redundant parametersAssignment tracking. No output change (all fixtures unchanged).
… output as .js

The fixture input is code.tsx but the compiled module output is JS (as in a real Storybook build). Set fixtureOutputExt to '.js' via options.json so the emitted output file is output.js.
@github-actions

Copy link
Copy Markdown

📊 Bundle size report

✅ No changes found

@github-actions

Copy link
Copy Markdown

Pull request demo site: URL

Copilot AI 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.

Pull request overview

This PR enhances @fluentui/babel-preset-storybook-full-source by adding an opt-in storyGranularity: 'story' mode that generates a per-story, sliced context.parameters.fullSource for multi-story CSF files, while preserving the existing 'file' default behavior.

Changes:

  • Adds storyGranularity?: 'file' | 'story' to plugin options and wires it into the fullSource Babel plugin.
  • Introduces a new sliceStorySource() implementation that slices to a single target story and prunes unused imports/helpers while normalizing CSF3 stories into renderable functions.
  • Adds fixture coverage for CSF2/CSF3 multi-story cases, spreads, args-only stories, data exports, and TS type reachability; excludes fixtures from lib/spec tsconfigs.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/react-components/babel-preset-storybook-full-source/tsconfig.spec.json Excludes fixture directories from spec compilation.
packages/react-components/babel-preset-storybook-full-source/tsconfig.lib.json Excludes fixture directories from library compilation.
packages/react-components/babel-preset-storybook-full-source/src/types.ts Adds the new storyGranularity option to the public options type.
packages/react-components/babel-preset-storybook-full-source/src/sliceStory.ts Adds per-story slicing + CSF normalization logic used by 'story' mode.
packages/react-components/babel-preset-storybook-full-source/src/fullsource.ts Implements 'file' vs 'story' handling and emits per-story fullSource.
packages/react-components/babel-preset-storybook-full-source/src/fullsource.test.ts Adds a new plugin-tester pass for per-story slicing fixtures.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/multi-story-typescript/output.js Fixture output asserting TS type reachability and pruning per story.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/multi-story-typescript/options.json Fixture config for .tsx input / .js output.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/multi-story-typescript/code.tsx Fixture input for TS + multiple CSF3 stories with different reachable types.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/multi-story-csf3/output.js Fixture output for multi-story CSF3 slicing/pruning.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/multi-story-csf3/code.js Fixture input for multi-story CSF3 slicing/pruning.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/multi-story-csf2/output.js Fixture output for CSF2 multi-story slicing/pruning.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/multi-story-csf2/code.js Fixture input for CSF2 multi-story slicing/pruning.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/csf3-spread/output.js Fixture output for CSF3 { ...Base } spread resolution.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/csf3-spread/code.js Fixture input for CSF3 { ...Base } spread resolution.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/csf3-render-and-args/output.js Fixture output for CSF3 render, merged args, and args-only synthesis.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/csf3-render-and-args/code.js Fixture input for CSF3 render, meta args, story args, and args-only story.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/csf3-data-export/output.js Fixture output ensuring capitalized non-story data exports are ignored.
packages/react-components/babel-preset-storybook-full-source/src/fixtures/storybook-stories-fullsource-per-story/csf3-data-export/code.js Fixture input containing both a story export and a capitalized data export.
packages/react-components/babel-preset-storybook-full-source/README.md Documents the new opt-in per-story mode and its behavior.
packages/react-components/babel-preset-storybook-full-source/etc/babel-preset-storybook-full-source.api.md Updates public API report for the new option.
change/@fluentui-babel-preset-storybook-full-source-perstory.json Adds a patch change file describing the new opt-in behavior.

Comment on lines +432 to +442
const argsParam = render.params[0];
const body = render.body;

// No args parameter — keep the body as-is.
if (!argsParam) {
return t.arrowFunctionExpression([], body);
}

const argsDeclaration = t.variableDeclaration('const', [
t.variableDeclarator(argsParam as Babel.types.LVal, mergedArgs ?? t.objectExpression([])),
]);
Comment on lines +716 to +726
function isStoryAnnotationAssignment(t: typeof Babel.types, statementPath: Babel.NodePath): boolean {
if (!statementPath.isExpressionStatement()) {
return false;
}
const expression = statementPath.node.expression;
return (
t.isAssignmentExpression(expression) &&
t.isMemberExpression(expression.left) &&
t.isIdentifier(expression.left.object)
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants