shared-module-extraction
Use when a duplication gate flags cloned component code — a jscpd failure, a qlty `similar-code` comment, or two components differing only in a path, a viewBox, or a stroke width — and the fix is to e
SKILL.mdplugins/react-frontend-sdlc/skills/shared-module-extraction/SKILL.md
GitHubShared module extraction
Profile keys consumed
make.lint_dupmake.test_visualcapabilities.visual_testingquality.jscpd_clonesquality.visual_diffsarchitecture.source_root
Overview
Extracting duplicated component internals is only half the job. Because the extraction changes what every consumer renders, the full visual suite is the proof: a run that rewrites no baseline means the rendered output is byte-identical and the extraction is safe.
When to use
- A duplication gate reports a clone of roughly twenty lines or more between components.
- Two icon or field wrappers share structure and differ only in data (path, viewBox, stroke width, label).
- A shared internals barrel already exists and a third consumer is about to hand-roll a fourth copy.
- Not for: incidental similarity below the gate's token threshold — abstracting that pushes toward a worse shape than the duplication.
Applicability gate
The proof step needs a visual suite. Skip step 4 and step 5 with a recorded note when
capabilities.visual_testing is false or the target mapped by make.test_visual is null, and
substitute the closest available render evidence (component snapshot tests, then a manual pass over
every consumer). When make.lint_dup maps to null there is no duplication gate to re-run, so skip
step 7 with a recorded note and confirm the clone is gone by reading both former sites.
Applicability by repository shape
- React SPA shape (feature modules under the source root, a bootable app, an aggregate CI
target): yes — the target mapped by
make.lint_dupruns jscpd at zero tolerance (.jscpd.json, 75 tokens / 5 lines over the source root), and the target mapped bymake.test_visualdrives the per-spec baselines under the visual specs' snapshot directories. - Next.js app shape (routed pages, no aggregate duplication gate): partial — no jscpd gate;
duplication surfaces only as a qlty
similar-codecomment, and the visual specs and baselines live under the test tree's visual directory. - Component-library shape (Storybook-first, no bootable app, published package): yes — shared
internals live in a field-controls directory behind its
index.tsbarrel, and the target mapped bymake.test_visualruns the chromium baselines under the visual test directory.
Procedure
Confirm it is a real clone. Read both sites and identify what varies. If the difference is data (a path string, a size, a label) the block is extractable; if the difference is behaviour, extract the shared part only.
Extract into the repo's shared internals location under
architecture.source_root, not into one of the consumers, and export it from that directory's barrel so consumers import through the public path.Replace every consumer, not just the two the gate named — leaving one copy behind keeps the gate red and re-splits the code path.
Run the full visual suite through the target mapped by
make.test_visual, not the specs for the components touched. A shared module reaches consumers the diff does not mention. Then read the working tree for rewritten baselines, under whichever visual test directory the repository keeps them in.# With a profile whose make.test_visual maps to `test-visual` and baselines under tests/visual: make test-visual git status --porcelain tests/visualRequire zero rewritten baselines. No
.pngshows as modified and no diff image is produced, so the rendered DOM is unchanged for every consumer. That is the pass condition, and it is the samequality.visual_diffsceiling of0that CI enforces.If a baseline does change, stop and diagnose. A rewritten snapshot means the extraction altered rendering — a lost prop, a changed default, a wrapper element added. Fix the extracted module until the suite is clean; only refresh baselines when the visual change is the intended outcome of a separate, reviewed change.
Re-run the duplication gate through the target mapped by
make.lint_dupto confirm the clone is gone rather than merely relocated —quality.jscpd_clonesis a ceiling of0.
Preventing the next clone
New inline markup of a kind the shared module already covers — another icon wrapper, another field adornment — reuses the shared module from the start. Hand-rolling a near-copy just re-enters the duplication gate on the next run and costs a second extraction plus a second full visual run.
Common mistakes
- Running only the specs for the components in the diff, so a third consumer regresses unnoticed.
- Accepting rewritten baselines as "close enough" — that converts an unreviewed rendering change into the new reference.
- Extracting behaviour along with markup, which turns one clone into a shared module with a flag argument for each caller.
- Leaving the last consumer un-migrated, so the gate stays red and both code paths must be maintained.
- Deduplicating by loosening the gate's token or line thresholds instead of removing the clone.