Skip to content

feat(codemod): add react-simplikit-codemod with the mobile-to-root transform - #458

Draft
mnxmnz wants to merge 26 commits into
mainfrom
454-mobile-to-root-codemod
Draft

feat(codemod): add react-simplikit-codemod with the mobile-to-root transform#458
mnxmnz wants to merge 26 commits into
mainfrom
454-mobile-to-root-codemod

Conversation

@mnxmnz

@mnxmnz mnxmnz commented Aug 31, 2026

Copy link
Copy Markdown
Member

Overview

Closes #454. Adds react-simplikit-codemod, a bin-only CLI that migrates consumers off @react-simplikit/mobile — absorbed into the react-simplikit root entry in #437 — onto the root entry.

npx react-simplikit-codemod mobile-to-root

What each reference to the old package becomes:

flowchart TD
    R["@react-simplikit/mobile reference"] --> W{found where}
    W -->|dependency field| M["swap to react-simplikit@^0.2.0"]
    W -->|resolutions or overrides| N["report only, meaning differs per package manager"]
    W -->|source import| E{file already imports react-simplikit}
    E -->|no| P["rewrite the specifier in place"]
    E -->|yes| S{"merging would rebind a name, or strand a comment"}
    S -->|no| F["fold the bindings into that import"]
    S -->|yes| C["rewrite in place, then report"]
Loading

Source files are parsed with the TypeScript compiler and edited by offset, so formatting, comments and quote style survive untouched; package.json is the exception and gets reserialized. The run never prompts and is idempotent — a second run finds nothing to change. --dry-run previews, --json emits a machine-readable report, and exit 1 means some file could not be read, parsed or written while every other file still migrated.

Full surface, flags and exit codes are in packages/codemod/README.md; the agent-facing JSON contract ships as a skill in packages/plugin/skills/react-simplikit-codemod/.

Verification

  • 120 unit tests at 100% coverage, plus 24 e2e tests that spawn the built bin
  • quality (codemod) added to integration.yml; the spec matrix already covers node 22 and 24

Checklist

  • Did you write the test code?
  • Have you run yarn run fix to format and lint the code and docs?
  • Have you run yarn run test:coverage to make sure there is no uncovered line?
  • Did you write the JSDoc? — bin-only package, no public API surface

@changeset-bot

changeset-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e0c48c4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
react-simplikit-codemod Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 104 kB

ℹ️ View Unchanged
Filename Size
packages/codemod/dist/cli.mjs 6.47 kB
packages/react-simplikit/dist/components/ImpressionArea/ImpressionArea.cjs 1.01 kB
packages/react-simplikit/dist/components/ImpressionArea/ImpressionArea.mjs 984 B
packages/react-simplikit/dist/components/Separated/Separated.cjs 686 B
packages/react-simplikit/dist/components/Separated/Separated.mjs 684 B
packages/react-simplikit/dist/components/SwitchCase/SwitchCase.cjs 701 B
packages/react-simplikit/dist/components/SwitchCase/SwitchCase.mjs 699 B
packages/react-simplikit/dist/hooks/useAsyncEffect/useAsyncEffect.cjs 626 B
packages/react-simplikit/dist/hooks/useAsyncEffect/useAsyncEffect.mjs 615 B
packages/react-simplikit/dist/hooks/useBooleanState/useBooleanState.cjs 539 B
packages/react-simplikit/dist/hooks/useBooleanState/useBooleanState.mjs 529 B
packages/react-simplikit/dist/hooks/useCallbackOncePerRender/useCallbackOncePerRender.cjs 790 B
packages/react-simplikit/dist/hooks/useCallbackOncePerRender/useCallbackOncePerRender.mjs 762 B
packages/react-simplikit/dist/hooks/useConditionalEffect/useConditionalEffect.cjs 955 B
packages/react-simplikit/dist/hooks/useConditionalEffect/useConditionalEffect.mjs 934 B
packages/react-simplikit/dist/hooks/useControlledState/useControlledState.cjs 865 B
packages/react-simplikit/dist/hooks/useControlledState/useControlledState.mjs 855 B
packages/react-simplikit/dist/hooks/useCounter/useCounter.cjs 1.03 kB
packages/react-simplikit/dist/hooks/useCounter/useCounter.mjs 1.01 kB
packages/react-simplikit/dist/hooks/useDebounce/debounce.cjs 458 B
packages/react-simplikit/dist/hooks/useDebounce/debounce.mjs 456 B
packages/react-simplikit/dist/hooks/useDebounce/useDebounce.cjs 974 B
packages/react-simplikit/dist/hooks/useDebounce/useDebounce.mjs 953 B
packages/react-simplikit/dist/hooks/useDebouncedCallback/useDebouncedCallback.cjs 1.36 kB
packages/react-simplikit/dist/hooks/useDebouncedCallback/useDebouncedCallback.mjs 1.34 kB
packages/react-simplikit/dist/hooks/useDoubleClick/useDoubleClick.cjs 965 B
packages/react-simplikit/dist/hooks/useDoubleClick/useDoubleClick.mjs 950 B
packages/react-simplikit/dist/hooks/useGeolocation/useGeolocation.cjs 2.02 kB
packages/react-simplikit/dist/hooks/useGeolocation/useGeolocation.mjs 2.02 kB
packages/react-simplikit/dist/hooks/useImpressionRef/useImpressionRef.cjs 1.12 kB
packages/react-simplikit/dist/hooks/useImpressionRef/useImpressionRef.mjs 1.08 kB
packages/react-simplikit/dist/hooks/useInputState/useInputState.cjs 663 B
packages/react-simplikit/dist/hooks/useInputState/useInputState.mjs 654 B
packages/react-simplikit/dist/hooks/useIntersectionObserver/useIntersectionObserver.cjs 972 B
packages/react-simplikit/dist/hooks/useIntersectionObserver/useIntersectionObserver.mjs 945 B
packages/react-simplikit/dist/hooks/useInterval/useInterval.cjs 837 B
packages/react-simplikit/dist/hooks/useInterval/useInterval.mjs 811 B
packages/react-simplikit/dist/hooks/useIsClient/useIsClient.cjs 585 B
packages/react-simplikit/dist/hooks/useIsClient/useIsClient.mjs 574 B
packages/react-simplikit/dist/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.cjs 577 B
packages/react-simplikit/dist/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.mjs 575 B
packages/react-simplikit/dist/hooks/useList/useList.cjs 952 B
packages/react-simplikit/dist/hooks/useList/useList.mjs 930 B
packages/react-simplikit/dist/hooks/useLoading/useLoading.cjs 919 B
packages/react-simplikit/dist/hooks/useLoading/useLoading.mjs 909 B
packages/react-simplikit/dist/hooks/useLongPress/useLongPress.cjs 1.68 kB
packages/react-simplikit/dist/hooks/useLongPress/useLongPress.mjs 1.64 kB
packages/react-simplikit/dist/hooks/useMap/useMap.cjs 730 B
packages/react-simplikit/dist/hooks/useMap/useMap.mjs 713 B
packages/react-simplikit/dist/hooks/useOutsideClickEffect/useOutsideClickEffect.cjs 789 B
packages/react-simplikit/dist/hooks/useOutsideClickEffect/useOutsideClickEffect.mjs 758 B
packages/react-simplikit/dist/hooks/usePreservedCallback/usePreservedCallback.cjs 691 B
packages/react-simplikit/dist/hooks/usePreservedCallback/usePreservedCallback.mjs 675 B
packages/react-simplikit/dist/hooks/usePreservedReference/usePreservedReference.cjs 805 B
packages/react-simplikit/dist/hooks/usePreservedReference/usePreservedReference.mjs 789 B
packages/react-simplikit/dist/hooks/usePrevious/usePrevious.cjs 643 B
packages/react-simplikit/dist/hooks/usePrevious/usePrevious.mjs 635 B
packages/react-simplikit/dist/hooks/useRefEffect/useRefEffect.cjs 780 B
packages/react-simplikit/dist/hooks/useRefEffect/useRefEffect.mjs 753 B
packages/react-simplikit/dist/hooks/useSet/useSet.cjs 1.03 kB
packages/react-simplikit/dist/hooks/useSet/useSet.mjs 1 kB
packages/react-simplikit/dist/hooks/useStorageState/storage.cjs 543 B
packages/react-simplikit/dist/hooks/useStorageState/storage.mjs 527 B
packages/react-simplikit/dist/hooks/useStorageState/useStorageState.cjs 966 B
packages/react-simplikit/dist/hooks/useStorageState/useStorageState.mjs 957 B
packages/react-simplikit/dist/hooks/useThrottle/throttle.cjs 306 B
packages/react-simplikit/dist/hooks/useThrottle/throttle.mjs 298 B
packages/react-simplikit/dist/hooks/useThrottle/useThrottle.cjs 864 B
packages/react-simplikit/dist/hooks/useThrottle/useThrottle.mjs 838 B
packages/react-simplikit/dist/hooks/useThrottledCallback/useThrottledCallback.cjs 1.22 kB
packages/react-simplikit/dist/hooks/useThrottledCallback/useThrottledCallback.mjs 1.19 kB
packages/react-simplikit/dist/hooks/useTimeout/useTimeout.cjs 627 B
packages/react-simplikit/dist/hooks/useTimeout/useTimeout.mjs 600 B
packages/react-simplikit/dist/hooks/useToggle/useToggle.cjs 524 B
packages/react-simplikit/dist/hooks/useToggle/useToggle.mjs 506 B
packages/react-simplikit/dist/hooks/useVisibilityEvent/useVisibilityEvent.cjs 692 B
packages/react-simplikit/dist/hooks/useVisibilityEvent/useVisibilityEvent.mjs 674 B
packages/react-simplikit/dist/index.cjs 1.37 kB
packages/react-simplikit/dist/index.mjs 997 B
packages/react-simplikit/dist/mobile/hooks/useAvoidKeyboard/useAvoidKeyboard.cjs 990 B
packages/react-simplikit/dist/mobile/hooks/useAvoidKeyboard/useAvoidKeyboard.mjs 968 B
packages/react-simplikit/dist/mobile/hooks/useBodyScrollLock/useBodyScrollLock.cjs 551 B
packages/react-simplikit/dist/mobile/hooks/useBodyScrollLock/useBodyScrollLock.mjs 527 B
packages/react-simplikit/dist/mobile/hooks/useKeyboardHeight/useKeyboardHeight.cjs 690 B
packages/react-simplikit/dist/mobile/hooks/useKeyboardHeight/useKeyboardHeight.mjs 671 B
packages/react-simplikit/dist/mobile/hooks/useNetworkStatus/useNetworkStatus.cjs 1.2 kB
packages/react-simplikit/dist/mobile/hooks/useNetworkStatus/useNetworkStatus.mjs 1.19 kB
packages/react-simplikit/dist/mobile/hooks/usePageVisibility/usePageVisibility.cjs 936 B
packages/react-simplikit/dist/mobile/hooks/usePageVisibility/usePageVisibility.mjs 917 B
packages/react-simplikit/dist/mobile/hooks/useSafeAreaInset/useSafeAreaInset.cjs 947 B
packages/react-simplikit/dist/mobile/hooks/useSafeAreaInset/useSafeAreaInset.mjs 932 B
packages/react-simplikit/dist/mobile/hooks/useScrollDirection/useScrollDirection.cjs 960 B
packages/react-simplikit/dist/mobile/hooks/useScrollDirection/useScrollDirection.mjs 949 B
packages/react-simplikit/dist/mobile/hooks/useVisualViewport/useVisualViewport.cjs 1.24 kB
packages/react-simplikit/dist/mobile/hooks/useVisualViewport/useVisualViewport.mjs 1.22 kB
packages/react-simplikit/dist/mobile/utils/disableBodyScrollLock/disableBodyScrollLock.cjs 681 B
packages/react-simplikit/dist/mobile/utils/disableBodyScrollLock/disableBodyScrollLock.mjs 673 B
packages/react-simplikit/dist/mobile/utils/enableBodyScrollLock/enableBodyScrollLock.cjs 677 B
packages/react-simplikit/dist/mobile/utils/enableBodyScrollLock/enableBodyScrollLock.mjs 666 B
packages/react-simplikit/dist/mobile/utils/getKeyboardHeight/getKeyboardHeight.cjs 613 B
packages/react-simplikit/dist/mobile/utils/getKeyboardHeight/getKeyboardHeight.mjs 607 B
packages/react-simplikit/dist/mobile/utils/getSafeAreaInset/getSafeAreaInset.cjs 907 B
packages/react-simplikit/dist/mobile/utils/getSafeAreaInset/getSafeAreaInset.mjs 898 B
packages/react-simplikit/dist/mobile/utils/isAndroid/isAndroid.cjs 547 B
packages/react-simplikit/dist/mobile/utils/isAndroid/isAndroid.mjs 538 B
packages/react-simplikit/dist/mobile/utils/isIOS/isIOS.cjs 758 B
packages/react-simplikit/dist/mobile/utils/isIOS/isIOS.mjs 751 B
packages/react-simplikit/dist/mobile/utils/isKeyboardVisible/isKeyboardVisible.cjs 434 B
packages/react-simplikit/dist/mobile/utils/isKeyboardVisible/isKeyboardVisible.mjs 423 B
packages/react-simplikit/dist/mobile/utils/isServer/isServer.cjs 386 B
packages/react-simplikit/dist/mobile/utils/isServer/isServer.mjs 384 B
packages/react-simplikit/dist/mobile/utils/subscribeKeyboardHeight/subscribeKeyboardHeight.cjs 1.12 kB
packages/react-simplikit/dist/mobile/utils/subscribeKeyboardHeight/subscribeKeyboardHeight.mjs 1.1 kB
packages/react-simplikit/dist/utils/buildContext/buildContext.cjs 809 B
packages/react-simplikit/dist/utils/buildContext/buildContext.mjs 788 B
packages/react-simplikit/dist/utils/mergeProps/mergeProps.cjs 671 B
packages/react-simplikit/dist/utils/mergeProps/mergeProps.mjs 670 B
packages/react-simplikit/dist/utils/mergeRefs/mergeRefs.cjs 616 B
packages/react-simplikit/dist/utils/mergeRefs/mergeRefs.mjs 614 B

compressed-size-action

@codecov-commenter

codecov-commenter commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (fd312f5) to head (e0c48c4).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##              main      #458    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           58        67     +9     
  Lines         1664      2184   +520     
  Branches       500       706   +206     
==========================================
+ Hits          1664      2184   +520     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- compare imported names, not local ones, so an alias collision falls back to a plain rewrite
- add react-simplikit per dependency field instead of once per manifest
- apply ignore globs, extension and package.json rules to explicitly named paths
- keep imports inside declare module blocks and imports carrying comments in place
- report manual follow-ups even when no file changed
- rewrite import-equals require, require.resolve and the vitest import helpers
… packed bin

- accumulate per-file failures into the run result instead of throwing, so the report
  survives and one bad manifest no longer sinks a dry run
- exit 1 with every failure named under "Could not be processed"
- add an e2e case that packs the tarball and runs the bin it declares
Codemod practice is to bail with a reason rather than leave a refusal invisible
(LibCST SkipFile, styled-components-to-stylex warnings). A silently unmerged
import invites the user to merge it by hand, which reintroduces exactly the
binding change the codemod avoided.

Reports the two refusals a reader could plausibly undo — a colliding local name
and an attached comment — with the line. Adds an idempotency test.
… on a wrapper

formatJson wrapped one JSON.stringify call in a module and a 62-line test. Three
of those four tests asserted JSON.stringify's own behaviour; the fourth pinned the
field set one layer above where a consumer reads it, so it could not see the CLI
picking the wrong formatter or anything else writing to stdout.

The key set is now asserted on the real process output. formatHuman keeps its unit
tests — it has five branches worth localizing.
- tie the merge guard to the range it actually deletes, and detect comments with
  the TypeScript scanner instead of a substring scan. aa72201 widened the range to
  the whole statement and dropped the doc comment stating the precondition that
  made the scan exact, so a banner above the first import blocked merges while a
  trailing comment on the import's own line was orphaned
- glob with dot: true so .storybook and friends are scanned; ignore .pnp.*
- anchor user ignore globs to cwd, so --ignore 'legacy/**' stops being a no-op
- raise an existing react-simplikit range that sits below the floor, keep
  non-registry protocols instead of replacing them with a caret range, and widen
  peer ranges with >= rather than narrowing them with ^
- report a file that names the package where no import could be rewritten
- only claim a clean scan when nothing came back at all
- read --debug from the parsed option and carry it into per-file failures

The formatHuman guard added in ed23039 asserted a string the function no longer
emits, so it could not fail; three manifest tests and two merge tests encoded the
old behaviour and were corrected against the reproductions.
- list the refusals the report names and the ones it does not, instead of
  claiming the first two of three are reported when it reports the first and third
- add `failed` to the documented --json object and state that all six keys are
  always present
- drop `npm install react-simplikit` from the post-run step: it overwrites the
  range the codemod just wrote, so a bare reinstall is what is wanted
- record the dot-directory scan, the cwd-relative --ignore, the raised version
  floor, the preserved non-registry protocols and the unclassifiable-mention note
- CLAUDE.md: add packages/codemod to the structure tree, and name all three
  coverage exclusions with the reason each one holds
- llms.txt: point agents at the codemod instead of stating a stale floor
- verifyDocsI18n: say what the assertion guards now that it guards six commands

Also corrects the refusal wording — merging strands a comment on its own line
rather than dropping it, which is what the reproduction showed.
…r producers

- FileResult becomes a discriminated union; the reporter branches on `kind`
  instead of the proxy predicate `dependencies.length > 0`
- dissolve the top-level `types.ts` bucket: each type moves next to its sole
  producer, and only the five shared across the transform's siblings stay in
  `mobileToRoot/types.ts`
- flatten `src/transforms/mobileToRoot/` to `src/mobileToRoot/`
- restore `formatJson` beside `formatHuman` so both renderers are siblings;
  the e2e keeps pinning the stdout contract
- the e2e resolves the CLI from the manifest `bin` instead of hard-coding
  `dist/cli.mjs`, so a `bin` left behind by a build-output rename now fails
…ed files

The reporter refactor changed the published output without the docs following:

- SKILL.md showed `changed[]` entries carrying both `changes` and
  `dependencies` with one always empty. Entries now carry `kind` and only the
  matching key, so the example and the key list are rewritten from real output
- SKILL.md claimed `dependencies[].added` is omitted when the range already
  satisfied the floor. It is `null`
- CLAUDE.md named `src/types.ts` among the coverage exclusions and
  `transforms/` in the package tree; neither exists. The dead `src/types.ts`
  entry is dropped from the vitest exclude list too
- name the merge condition instead of "where it can", and call the target the
  root entry throughout

Also: report a target outside the current directory by its absolute path
rather than a chain of `..` segments, and say "1 file" instead of "1 file(s)".
…f the npm tarball

SKILL.md sat in packages/codemod but `files` is `dist/**/*`, so it never
reached the published tarball — and an npm tarball is not where any agent
looks for a skill. It moves to packages/plugin/skills/react-simplikit-codemod,
next to the generated catalog skill, and ships through the marketplace the
plugin already publishes.

The two skills are maintained differently, so `verifySkill` now separates the
checks: the frontmatter and length limits apply to both, while the
regeneration diff stays on the generated one. It also pins the version floor —
`MIN_RUNTIME_VERSION` is repeated in prose in two files, and it was already
wrong once at 0.1.1.
…y component

`isBelowFloor` was `FLOOR.some((part, index) => version[index] < part)`, which
asks whether ANY component is below its own floor component rather than letting
the first differing component decide. Against a 0.2.0 floor that ranks 1.0.0,
1.1.0, 2.0.0 and 10.0.0 as below it, so the codemod overwrote a consumer's
`react-simplikit` range with `^0.2.0` — a downgrade across a major, reported in
the same words as the documented floor raise. `^1.5.0` survived only because 5
is not below 2.

The same line treated "contains three dot-separated digits" as "is a registry
range", so `workspace:^0.1.1` and `file:../pkg-0.1.1.tgz` were flattened to a
registry range while their digit-free forms were kept. Protocol specs are now
detected by their scheme, kept on both sides of the swap, and reported — which
is what README.md already promised.

Latent today: react-simplikit is at 0.1.0, and for an all-0.x world the old
predicate collapses to a correct `minor < 2`. It goes live the day a major
ships while consumers are still running the published codemod.

The existing tests picked `^0.1.0` and `^0.9.0` — same major, so 100% coverage
and the bug coexisted. The new cases cover a higher major with a lower minor,
both protocol forms, the floor boundary itself, and a wildcard range.
`--ignore ./legacy/**` filtered nothing. Scoping joined the pattern onto the
cwd unchanged, and the resulting `/./` never matches in micromatch, so the run
reported no error and skipped no file. The leading `./` is now stripped before
the join.

`--debug` replaced the failure reason with a raw stack rather than adding to
it, so asking for more detail lost the actionable line — "package.json is not
valid JSON" became a bare SyntaxError. The stack now follows the reason.

The test that covered `--debug` asserted the reason starts with `Error: `,
which pinned the old behaviour rather than the intent; it now asserts the
plain reason survives inside the debug one.
…s on

`codemod-node-floor` ran on `node: ['22']`, which resolves to the newest 22.x
and never exercises the `>=22.17.0` in engines — the floor the job is named
for. It now pins 22.17.0.

The MIN_RUNTIME_VERSION guard added with the skill move matched a bare
`0.2.0`, which the illustrative JSON sample in SKILL.md also contains, so the
prose around it could go stale with the assertion still green. It now matches
the prose form `react-simplikit@0.2.0`.

Also: the manual-note sentence said the codemod could not be rewritten when it
meant the reference could, and SKILL.md documented one of the two conditions
that produce `added: null`.
…ngines floor

Pinning the floor job to 22.17.0 turned it red: tsdown fails to import `unrun`
on that version. But engines.node constrains the runtime a consumer runs the
published bin on, not the toolchain that builds it — a consumer never runs
tsdown. Building on the floor was never what the job was for.

The build now runs on the repo's .nvmrc node and only the CLI is exercised on
the floor, which is what the job's name has claimed all along. `test:e2e:run`
is `test:e2e` without the build step, for the second half.
The pre-existing `spec` matrix already runs `test:coverage` across every
workspace on node 22 and 24, so the codemod's unit tests have been covered on
node 22 since the workspace was added. The bespoke job only added the e2e on
22.17.0 rather than 22.latest — and that floor is a number this branch itself
picked, so the job largely existed to defend its own constant.

Removing it also stops the e2e running twice per PR. `quality (codemod)` keeps
it, and that is the only job that exercises the built bin.
Removes the `ponytail:` marker, which named a review tool rather than anything
about this repo, and drops the comments that restate their own code. What
stays is the handful a reader cannot recover: why the floor must name a
published version, why protocols are detected by scheme instead of digits, why
the floor comparison is not per-component, why a leading `./` is stripped, why
the skill gate matches the prose form, and why the e2e reads `bin` from the
manifest. Each guards a simplification that would reintroduce a bug this
branch fixed.
`engines.node: ">=22.17.0"` was picked when this package was scaffolded and
nothing verified it: the spec matrix runs node 22 and 24, never that patch.
react-simplikit declares no engines at all, so the CLI now makes no claim
either rather than an untested one. npm treats the field as advisory by
default, so removing it changes no install that was working.

The skill's Node precondition goes with it, for the same reason.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a codemod for migrating off @react-simplikit/mobile

2 participants