Skip to content

chore(storybook): enforce runnable "Show code" SFC via shared helper#688

Open
HerbertJulio wants to merge 3 commits into
devfrom
chore/storybook-runnable-show-code
Open

chore(storybook): enforce runnable "Show code" SFC via shared helper#688
HerbertJulio wants to merge 3 commits into
devfrom
chore/storybook-runnable-show-code

Conversation

@HerbertJulio

Copy link
Copy Markdown
Contributor

Problem

The Docs "Show code" panel emitted snippets a consumer could not copy-paste and run. Storybook's dynamic Vue source has two defects:

  1. Lowercases / kebab-cases the tag — a <Skeleton> in the canvas is emitted as <skeleton>, which does not resolve to the imported component.
  2. Already wraps in <template> — a naive transform wraps again, producing a nested <template><template>…</template></template>.

Observed (real "Show code" before this PR):

<script setup>
import Skeleton from '@aziontech/webkit/skeleton'
</script>

<template>
  <template>
    <skeleton animated height="100px" kind="shape" width="240px" />
  </template>
</template>

The snippet must express 100% of what the canvas renders and run as-is.

What this PR does

A single sanctioned path for "Show code", impossible to bypass without the hook blocking it:

  • Shared helperapps/storybook/src/stories/_shared/story-source.js
    • runnableDocs({ component, imports, components }) → spread into parameters.docs; sets description.component, source.transform, canvas.sourceState: 'shown'.
    • sfcTransform → unwraps the stray <template> and restores PascalCase tags.
    • toSfc(imports, body) → full SFC for composite stories' source.code.
  • Rule.claude/rules/storybook-source.md: the well-defined contract (single runnable SFC, PascalCase tags, no hand-rolled transform, no nested <template>, sourceState: 'shown').
  • Enforcement hook.claude/hooks/validate-story-source.mjs (registered PreToolUse) blocks hand-rolled transforms, lowercase/kebab tags of imported components, nested <template>, and new stories that bypass the helper. Only newly introduced violations block, so legacy stories are migrated as touched.
  • Authoring docsstorybook-write skill + COMPONENT_REQUIREMENTS § 13 now mandate the helper (older "append the Usage block" guidance superseded).
  • Migrated the three stories that had hand-rolled transforms: Skeleton, Badge, Chip.
  • Tests — 5 cases for the hook in .claude/hooks/__tests__/run.mjs (all pass).

After:

<script setup>
import Skeleton from '@aziontech/webkit/skeleton'
</script>

<template>
  <Skeleton animated height="100px" kind="shape" width="240px" />
</template>

Notes

  • The hooks test harness has 2 pre-existing failures (lib: legacy whitelist, enforce-spec-exists) in files this PR does not touch; the 5 new validate-story-source tests pass.
  • The matching fix for the Divider story shipped in [ENG-46312] feat(webkit): add Divider (layout) #678 (that branch predates this helper; it will adopt the helper once it re-syncs dev).

The Docs "Show code" panel emitted snippets that could not be pasted and run:
Storybook's dynamic Vue source lowercases the tag (`<skeleton>` instead of
`<Skeleton>`) and a naive transform wrapped it in a nested `<template>`. The
snippet must express 100% of the canvas and run as-is in a consumer app.

- add apps/storybook/src/stories/_shared/story-source.js (toSfc, sfcTransform,
  runnableDocs): one sanctioned way to emit a single runnable PascalCase SFC;
  the transform unwraps the stray <template> and restores PascalCase tags
- add .claude/rules/storybook-source.md: the well-defined rule
- add .claude/hooks/validate-story-source.mjs (registered PreToolUse): blocks
  hand-rolled transforms, lowercase/kebab tags, nested <template>, and new
  stories that bypass the helper; only newly introduced violations block
- update storybook-write skill + COMPONENT_REQUIREMENTS to mandate the helper
- migrate Skeleton, Badge, Chip stories to runnableDocs / toSfc
- cover the hook with tests in .claude/hooks/__tests__/run.mjs
@HerbertJulio HerbertJulio requested a review from a team as a code owner June 26, 2026 21:34
…ource

Root cause of the broken/inconsistent "Show code": Storybook's dynamic Vue
source emits the tag from the .vue file name (skeleton.vue -> <skeleton>,
chips.vue -> <chips>, not the PascalCase import), double-wraps <template>, and
when `parameters.docs` is a function call it prints the raw CSF story object
instead of the snippet.

- story-source.js: reduce to a single `toSfc(imports, body)` SFC builder; drop
  the dynamic `sfcTransform` / `runnableDocs` path entirely
- every story now declares an explicit `parameters.docs.source.code = toSfc(...)`
  with PascalCase tags; `parameters.docs` stays a plain object literal
- Skeleton, Badge, Chip migrated to the explicit pattern (Chip <chips> fixed)
- rule, skill, COMPONENT_REQUIREMENTS rewritten to the explicit pattern
- hook now blocks: docs-as-function-call, any source.transform, lowercase/kebab
  tag, nested <template>, and (new files) missing toSfc / sourceState
- hook tests updated
…ency

The component shipped plural on every surface (chips.vue, ./chips, name 'Chips',
testid input-chips) but the story imported it singular (`import Chip from
'@aziontech/webkit/chips'`). A component must have ONE name across all surfaces.

- rename to singular `chip`: folder/file, export ./chip, defineOptions name
  'Chip', data-testid 'input-chip', spec chip.md (name + checksum), story import
- new guardrail in validate-story-source.mjs: a story import binding must equal
  PascalCase(export subpath) — blocks `import Chip from '.../chips'`
- .claude/rules/naming.md: the single-source-of-truth invariant (one kebab name
  -> spec, file, export, defineOptions, testid, story binding) and what enforces
  each surface (validate-spec-compliance for internals, validate-story-source
  for the story binding)
- hook test for the binding mismatch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant