All files / packages/sds-customization-compliance/src story-key.ts

100% Statements 19/19
100% Branches 20/20
100% Functions 4/4
100% Lines 14/14

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82                                                                                      16x 11x 11x 10x       7x                               11x   9x     11x 7x 11x   14x   11x 11x 3x    
/**
 * Story → component-key resolution.
 *
 * The CLI writes one report per component, named with the camelCase
 * component key (e.g. `avatarGroup.json`, `progressIndicator.json`).
 * Storybook stories title themselves on a different convention —
 * typically "SLDS2 Components/<Display Name>" — so we walk the second
 * title segment back to its camelCase form to locate the artifact.
 *
 * Lives in the compliance package (not in any host) so every consumer
 * resolving a story to a per-component file (Storybook addons, MDX doc
 * blocks, sandbox routes …) reads from one source of truth.
 */
 
/**
 * Story-like input the resolver accepts. Loose by design: covers prepared
 * payloads (`{ title, ... }`), Storybook `meta` objects, and direct calls
 * with arbitrary objects that share the same fields.
 */
export interface StoryLike {
  title?: string;
  componentName?: string;
}
 
/**
 * Storybook manager-api shape used by addon panels — passed in as `api`
 * so the resolver can read the active story when no payload is in hand.
 */
export interface StoryApi {
  getCurrentStoryData(): unknown;
}
 
/**
 * Lower-camelCase a multi-word display name (e.g. `"Avatar Group"` →
 * `"avatarGroup"`). Trims and collapses whitespace; mid-word characters
 * are normalized to lowercase so screaming-snake or all-caps display
 * names collapse into the canonical key the CLI writes.
 *
 * Returns `null` when the input has no word characters, so callers can
 * key empty/whitespace inputs off the same `null` sentinel as a missing
 * `title` segment.
 */
export function toComponentKey(name: string | null | undefined): string | null {
  if (!name) return null;
  const parts = name.trim().split(/\s+/).filter(Boolean);
  if (!parts.length) return null;
  return (
    parts[0].toLowerCase() +
    parts
      .slice(1)
      .map((p) => p[0].toUpperCase() + p.slice(1).toLowerCase())
      .join('')
  );
}
 
/**
 * Resolve the component key from either:
 *   - a manager-api instance with `getCurrentStoryData()` (addons), or
 *   - a story-like payload (`STORY_PREPARED`, `meta`, etc.)
 *
 * Strategy: title's second segment first ("SLDS2 Components/<Name>"),
 * falling back to `componentName` when titles don't carry a slash.
 * Returns `null` when no key can be derived so consumers can render an
 * empty-state without throwing.
 */
export function deriveComponentKey(input: StoryApi | StoryLike | null | undefined): string | null {
  if (input === null || typeof input !== 'object') return null;
  const story =
    'getCurrentStoryData' in input && typeof input.getCurrentStoryData === 'function'
      ? (input.getCurrentStoryData() as StoryLike | null | undefined)
      : (input as StoryLike);
  if (!story) return null;
  const title = typeof story.title === 'string' ? story.title : '';
  const segments = title
    .split('/')
    .map((s) => s.trim())
    .filter(Boolean);
  const second = segments[1];
  if (second) return toComponentKey(second);
  return toComponentKey(story.componentName ?? null);
}