Skip to content

Duplicate Code: Builder Options Resolution Pattern in Framework Presets #34073

@github-actions

Description

@github-actions

Analysis of commit 533dcc0

Assignee: @copilot

Summary

The same ~12-line pattern for resolving builder options from the framework preset config is duplicated across 6 framework preset files. An existing utility function getBuilderOptions in code/core/src/common/utils/get-builder-options.ts already implements similar logic but is not used by these presets.

Duplication Details

Pattern: core export builder options resolution

  • Severity: Medium

  • Occurrences: 6 instances

  • Locations:

    • code/frameworks/react-webpack5/src/preset.ts (lines 13–24)
    • code/frameworks/server-webpack5/src/preset.ts (lines 7–18)
    • code/frameworks/angular/src/preset.ts (lines 36–46)
    • code/frameworks/ember/src/preset.ts (lines 43–53)
    • code/frameworks/nextjs/src/preset.ts (lines 24–50)
    • code/frameworks/nextjs-vite/src/preset.ts (lines 22–35)
  • Code Sample (representative, from server-webpack5/src/preset.ts):

    export const core: PresetProperty<'core'> = async (config, options) => {
      const framework = await options.presets.apply('framework');
    
      return {
        ...config,
        builder: {
          name: import.meta.resolve('`@storybook/builder-webpack5`'),
          options: typeof framework === 'string' ? {} : framework.options.builder || {},
        },
        renderer: import.meta.resolve('`@storybook/server`/preset'),
      };
    };

The near-identical block above is copy-pasted with only the name and renderer values differing across all 6 files.

Existing Utility (code/core/src/common/utils/get-builder-options.ts):

export async function getBuilderOptions(T extends Record<string, any)>(
  options: Options
): Promise(T | Record<string, never)> {
  const framework = await options.presets.apply('framework', {}, options);

  if (typeof framework !== 'string' && framework?.options?.builder) {
    return framework.options.builder;
  }

  const { builder } = await options.presets.apply('core', {}, options);

  if (typeof builder !== 'string' && builder?.options) {
    return builder.options as T;
  }

  return {};
}

This utility already handles framework.options.builder extraction, yet the framework presets re-implement this logic inline.

Impact Analysis

  • Maintainability: Any change to how builder options are resolved must be made in 6 separate files. A bug fix applied to one preset will not automatically propagate to the others.
  • Bug Risk: The inline variants use framework.options.builder || {} while getBuilderOptions uses optional chaining (framework?.options?.builder), creating subtle inconsistency. If the resolution logic ever needs updating, divergence is likely.
  • Code Bloat: ~12 lines × 6 files = ~72 lines of near-identical logic that could be reduced to a single shared helper or extended utility.

Refactoring Recommendations

  1. Extend getBuilderOptions or create a focused helper

    • Consider extracting a resolveBuilderOptions(framework) helper in code/core/src/common/utils/get-builder-options.ts that takes the already-resolved framework value (rather than options) so it can be called within the core preset export:
      export function extractBuilderOptions(framework: string | { options?: { builder?: unknown } }) {
        return typeof framework === 'string' ? {} : (framework.options?.builder ?? {});
      }
    • Estimated effort: Low (1–2 hours)
    • Benefits: Single source of truth for builder options resolution; eliminates 6 duplicate implementations
  2. Use the helper in each framework preset

    • Update all 6 framework preset files to import and call the shared helper:
      import { extractBuilderOptions } from 'storybook/internal/common';
      
      export const core: PresetProperty<'core'> = async (config, options) => {
        const framework = await options.presets.apply('framework');
        return {
          ...config,
          builder: {
            name: import.meta.resolve('`@storybook/builder-webpack5`'),
            options: extractBuilderOptions(framework),
          },
          renderer: import.meta.resolve('`@storybook/server`/preset'),
        };
      };
    • Estimated effort: Low (1–2 hours)
    • Benefits: Consistent behavior, easier testing, single fix point

Implementation Checklist

  • Review duplication findings
  • Add extractBuilderOptions (or similar) to code/core/src/common/utils/get-builder-options.ts and export it from storybook/internal/common
  • Refactor code/frameworks/react-webpack5/src/preset.ts
  • Refactor code/frameworks/server-webpack5/src/preset.ts
  • Refactor code/frameworks/angular/src/preset.ts
  • Refactor code/frameworks/ember/src/preset.ts
  • Refactor code/frameworks/nextjs/src/preset.ts
  • Refactor code/frameworks/nextjs-vite/src/preset.ts
  • Update tests
  • Verify no functionality broken

Analysis Metadata

  • Analyzed Files: 6 framework preset files + 1 existing utility
  • Detection Method: Serena semantic code analysis + pattern search
  • Commit: 533dcc0
  • Analysis Date: 2026-03-09T08:28:00Z

Generated by Duplicate Code Detector ·

To install this agentic workflow, run

gh aw add github/gh-aw/.github/workflows/duplicate-code-detector.md@852cb06ad52958b402ed982b69957ffc57ca0619

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions