Skip to content

chore: migrate to the magic tooling stack (pnpm + oxlint + oxfmt) - #9

Merged
GSTJ merged 11 commits into
masterfrom
chore/magic-tooling
Jul 27, 2026
Merged

chore: migrate to the magic tooling stack (pnpm + oxlint + oxfmt)#9
GSTJ merged 11 commits into
masterfrom
chore/magic-tooling

Conversation

@GSTJ

@GSTJ GSTJ commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Migrates the repo to the shared GSTJ/magic tooling stack.

Stack

Before After
yarn 1 + .yarnrcscripts/bootstrap.js, two lockfiles pnpm 11.17.0 workspace, one lockfile, packageManager pinned
eslint 8 + @react-native-community/eslint-config oxlint@1.75.0 + magic-oxlint-config/react-native
prettier 3 + eslint-config-prettier + eslint-plugin-prettier oxfmt@0.60.0 + magic-oxfmt-config (reactNative)
hand-rolled tsconfig.json magic-tsconfig/base.json
hand-written lint/typecheck/test/build CI steps GSTJ/magic/.github/workflows/ci.yml@main
no renovate config renovate.json extending github>GSTJ/magic

Scripts are now the standard set: lint, lint:fix, format, format:fix, typecheck, plus build, test and the existing release. typescript is gone — it is typecheck now.

The example app moved into the workspace

example/ is a pnpm workspace package, which broke two things that assumed a private example/node_modules:

  • main pointed at node_modules/expo/AppEntry.js. Replaced with the documented registerRootComponent entry, example/index.ts.
  • metro.config.js used blockList + extraNodeModules to stop a second copy of every peer dependency loading out of the repo root. One install means one copy, so it is now the plain Expo monorepo config (watchFolders + nodeModulesPaths).

Verified by actually bundling it: expo export --platform ios produces a 1144-module bundle, resolving react-native-magic-toast through the babel alias to src/ as before.

Renames

5, applied with magic-kebab --write (0 skipped, 0 conflicts, 0 needing review) and committed on their own:

src/@types/MagicToastProps.ts  -> src/@types/magic-toast-props.ts
src/assets/AlertIcon.tsx       -> src/assets/alert-icon.tsx
src/assets/SuccessIcon.tsx     -> src/assets/success-icon.tsx
src/components/AlertToast.tsx  -> src/components/alert-toast.tsx
src/components/SuccessToast.tsx-> src/components/success-toast.tsx

6 import specifiers rewritten. example/App.tsx stays — the react-native preset exempts it, because Expo's AppEntry.js imports ../../App from inside node_modules.

Violations fixed

18 diagnostics on the first run. Beyond the 5 filename ones:

  • react-native/no-color-literals (5) — the library's three colours move to src/colors.ts, the example app's two to a const at the top of App.tsx. The fill="white" icon props use the same constant.
  • typescript/consistent-type-imports (5) — autofixed to top-level import type.
  • react/function-component-definition (1)example/App.tsx is an arrow function with a separate default export.
  • jest/valid-title (2) — "renders alerts correctly" → "renders an alert toast". Snapshot keys were renamed in place rather than regenerated, so jest --ci passing proves the rendered output is byte-identical.

Then oxfmt . across the repo. Most of that diff is double quotes (the old prettier block set singleQuote: true) and package.json key sorting, which oxfmt does by default. No package.json value changed except the keywords array order.

One local workaround

oxlint.config.mts casts the imported preset: extends: [reactNative as OxlintConfig]. magic-oxlint-config@1.0.0 types overrides[].plugins as string[], while oxlint 1.75.0 narrows OxlintOverride["plugins"] to a union of known plugin names, so the README's defineConfig({ extends: [reactNative] }) fails tsc --noEmit with TS2322. Runtime is fine — oxlint loads the config and reports the preset's rules. Reported upstream; the cast comes out when the types line up.

Checks

pnpm install --frozen-lockfile from a clean tree, pnpm run build, pnpm run lint (0 diagnostics), pnpm run format (clean), pnpm run typecheck, pnpm test (2 passed, 2 snapshots), plus tsc --noEmit inside example/ and the expo export above. All green.

Note, unrelated and left alone: the jest moduleNameMapper maps \.svg to <rootDir>/src/__mocks__/svgMock.ts, a file that does not exist on master either. Nothing imports an .svg — the icons are inline react-native-svg components — so it never fires.

https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1

GSTJ added 7 commits July 27, 2026 02:37
Replaces yarn with pnpm (workspace, `packageManager` pin, single lockfile)
and drops eslint, @react-native-community/eslint-config, prettier and its
two eslint bridges for oxlint 1.75.0, oxfmt 0.60.0 and the shared
magic-oxlint-config / magic-oxfmt-config / magic-tsconfig packages.

The `example/` app becomes a workspace package, which changes two things it
depended on:

- `main` pointed at `node_modules/expo/AppEntry.js`, a path that no longer
  exists once pnpm hoists to the workspace root. Replaced with the
  documented `registerRootComponent` entry in `example/index.ts`.
- `metro.config.js` used a blockList/extraNodeModules pair to stop a second
  copy of every peer dependency being loaded out of the root. One install
  means one copy, so it is now the standard Expo monorepo config
  (`watchFolders` + `nodeModulesPaths`).

`.yarnrc` and `scripts/bootstrap.js` go with yarn; `pnpm install` already
does what `yarn bootstrap` did.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
`oxlint.config.mts` extends the `react-native` preset and re-declares its
`ignorePatterns` — oxlint does not inherit those through `extends` — plus
`lib/`, the bob build output.

`tsconfig.json` extends `magic-tsconfig/base.json`; `tsconfig.build.json`
pins `rootDir` to the repo root so bob keeps emitting declarations at
`lib/typescript/src/index.d.ts`, which is where package.json `types` points.

`.nvmrc` records the Node version CI already used, so the reusable workflow
can read it.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
`unicorn/filename-case` is on at kebabCase in the shared preset. Applied
with `magic-kebab --write`: 5 renames, 6 import specifiers rewritten, 0
skipped, 0 conflicts, 0 needing review.

`example/App.tsx` stays as it is — the react-native preset exempts it and
the codemod skips it, because Expo's `AppEntry.js` imports `../../App` from
inside node_modules where nothing can rewrite it.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
Eighteen diagnostics, five of them the filename renames already committed.
The rest:

- `typescript/consistent-type-imports` (5) — autofixed to top-level
  `import type`.
- `react-native/no-color-literals` (5) — the library's three colours move to
  `src/colors.ts`, the example app's two to a const at the top of `App.tsx`.
  The icon `fill="white"` props use the same constant, since it is the same
  decision.
- `react/function-component-definition` (1) — `example/App.tsx` becomes an
  arrow function with a separate default export.
- `jest/valid-title` (2) — "renders alerts correctly" says nothing "renders
  an alert toast" doesn't. Snapshot keys renamed in place rather than
  regenerated, so `jest --ci` proves the rendered output is unchanged.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
First run of the shared formatter. Two things account for most of the diff:
oxfmt's house style is Prettier's defaults (double quotes, where the old
`prettier` block in package.json set `singleQuote: true`), and oxfmt sorts
`package.json` keys — the values are untouched, only the order and the
`keywords` array changed.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
The job did checkout / setup-node with a yarn cache / install / lint /
typescript / test / prepare by hand. All of that now comes from
`GSTJ/magic/.github/workflows/ci.yml`, which adds a format check and reads
the Node version from `.nvmrc` and pnpm from `packageManager`.

Build and test stay opted in, so the workflow still fails on a broken build
or a broken test. Turbo caching is off — there is no turbo here.

`renovate.json` extends `github>GSTJ/magic`, which is where the grouping
rules that keep oxlint and oxfmt pinned together live.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
magic-oxlint-config, magic-oxfmt-config, magic-tsconfig and magic-codemods
all move from 1.0.0 to 1.1.0.

The minimumReleaseAgeExclude entries move with them. pnpm 11 verifies the
lockfile against the 24h quarantine before it resolves anything, so the
1.0.0 entries had to stay in place for the install that removed them, then
come out afterwards.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
@GSTJ

GSTJ commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Updated to the patched packages: magic-oxlint-config, magic-oxfmt-config, magic-tsconfig and magic-codemods all at 1.1.0. Branch is green again: pnpm install --frozen-lockfile from an empty node_modules, bob build, oxlint 0 diagnostics, oxfmt --check clean, tsc --noEmit at the root and in example/, 2 jest tests.

Workarounds removed

The as OxlintConfig cast is gone. 1.1.0 types plugins as oxlint's own 15-member union and rules as its rule-entry shape, so the preset object is assignable without help.

ignorePatterns still does not survive extends on oxlint 1.75.0. Verified with --print-config: defineConfig({ extends: [reactNative] }) reports "ignorePatterns": []. So instead of keeping the re-declaration, the config now goes through extendConfig, which flattens the preset and the local object into one config and unions the arrays. **/lib/**, bob's output directory, is the only pattern this repo declares now, and --print-config shows all 19.

magic-tsconfig 1.1.0 drops incremental, so *.tsbuildinfo came out of .gitignore and the two stale files on disk are deleted. rm -rf lib && pnpm run build emits properly.

There was nothing else to strip: no local rule overrides in this repo, no oxlint-disable comments in src/, no Next.js config.

New violations

typescript/consistent-type-definitions now carries the "type" option, which turned three interface declarations into errors: MagicToastProps, MagicToastStyles, ContainerProps. oxlint --fix converted all three, ContainerProps extends ViewProps into an intersection, and oxfmt added the trailing semicolons. Nothing else in the 1.1.0 rule changes fires on this code.

Notes

pnpm 11 verifies the committed lockfile against the release quarantine before it resolves anything, so pnpm install fails on lockfile entries that pnpm-workspace.yaml no longer excludes. Swapping minimumReleaseAgeExclude straight from magic-*@1.0.0 to @1.1.0 gets you ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION naming the four 1.0.0 entries. Both sets have to sit in the file for the one install that swaps them, and the 1.0.0 lines come out after.

oxlint --print-config renders rules inherited through extends without their options, e.g. "unicorn/filename-case": "deny" in place of the configured object with its ignore list. The options do apply at runtime; I ran both config shapes over src/ and got identical diagnostics. ignorePatterns is the only field genuinely lost.

magic-kebab --dry-run --strict on 1.1.0: 0 renames, 0 conflicts, 0 needing review, and tsconfig discovery now finds example/tsconfig.json alongside the root one.

GSTJ added 2 commits July 27, 2026 05:36
The `as OxlintConfig` cast is gone. 1.1.0 types `plugins` as oxlint's own
15-member union and `rules` as its rule-entry shape, so the preset object is
assignable to `extends` without help.

`extends` still drops ignorePatterns on oxlint 1.75.0. `--print-config` on
`defineConfig({ extends: [reactNative] })` reports `"ignorePatterns": []`, so
the re-declaration could not just be deleted. The config now goes through
`extendConfig` instead, which flattens the preset and the local object into
one config and unions the arrays. That leaves `**/lib/**`, bob's output
directory, as the only thing this repo declares.

magic-tsconfig 1.1.0 drops `incremental`, so nothing writes a .tsbuildinfo
any more and the .gitignore entry goes with it.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
typescript/consistent-type-definitions ships with the "type" option in
magic-oxlint-config 1.1.0, so every `interface` is now an error. `oxlint
--fix` handled all three; oxfmt added the trailing semicolons.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
@GSTJ
GSTJ force-pushed the chore/magic-tooling branch from 02bab18 to 94d877a Compare July 27, 2026 08:36
GSTJ added 2 commits July 27, 2026 06:23
magic-oxlint-config, magic-oxfmt-config and magic-tsconfig go to 1.2.0.
magic-codemods stays on 1.1.0 and magic-oxlint-plugin is not a dependency
here.

Nothing in the release changes what this repo runs. The oxlint fix mirrors
env/globals into a `files: ["**"]` override so they survive oxlint's
`extends`; this repo goes through `extendConfig`, which flattens instead, so
it already had both. `incremental` is back in `nextjs.json` only, and this
repo extends `base.json` and `expo.json`, so no .tsbuildinfo appears and the
.gitignore entry dropped in the 1.1.0 round stays dropped. oxfmt's new
`withoutIgnorePatterns()` is for repos that hand-write CHANGELOG.md; this one
generates it from release-it and wants the default ignore.

The exclusions were listed at both 1.1.0 and 1.2.0 for the install that
rewrote the lockfile, because pnpm verifies the committed lockfile against
the release-age policy before resolving anything, then cut back to 1.2.0 and
confirmed with --frozen-lockfile. Lockfile diff is 17 lines each way and
touches nothing but the three packages.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
1.2.0 documents that `oxlint --print-config` renders an `extends`-shaped
config post-expansion and pre-merge, so the `"ignorePatterns": []` it prints
is not evidence of what would actually run. The conclusion here was right for
a different reason: `extends` replaces the preset's ignorePatterns with the
local array, and oxlint has no per-override ignore to put them back with.
Config is unchanged.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
@GSTJ

GSTJ commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Bumped onto the magic 1.2.0 round: magic-oxlint-config, magic-oxfmt-config and magic-tsconfig to ^1.2.0. magic-codemods stays at 1.1.0, and magic-oxlint-plugin isn't a dependency here.

Nothing in the release changes behaviour for this repo, and I checked rather than assumed:

  • env/globals surviving extends — this config uses extendConfig, which flattens the preset instead of going through oxlint's extends, so env and globals were already intact. Lint still reports 0/0 across 21 files with 432 rules.
  • incremental back in nextjs.json — this repo extends base.json and expo.json, neither of which sets it. No .tsbuildinfo appears after a build and the working tree stays clean, so the .gitignore entry dropped in the 1.1.0 round stays dropped. There was no hand-revert here to undo.
  • withoutIgnorePatterns() — for repos that write CHANGELOG.md by hand. This one generates it via release-it, and nothing in the release script calls oxfmt on it, so the default **/CHANGELOG.md ignore is what we want.

One doc fix: the comment in oxlint.config.mts justified extendConfig by citing oxlint --print-config output, which 1.2.0 documents as an invalid way to audit an extends-shaped config. The conclusion was right for a different reason, so the comment now says that instead. Config unchanged.

The pnpm 11 exclusions were listed at both 1.1.0 and 1.2.0 for the single install that rewrote the lockfile, then cut back to 1.2.0 and confirmed with --frozen-lockfile. The lockfile diff is 17 lines each way and touches only the three packages.

Green: install (clean + frozen), build, lint 0/0, oxfmt --check on 33 files, typecheck (root and example), 2 tests / 2 snapshots.

@GSTJ
GSTJ merged commit f4eeace into master Jul 27, 2026
1 check passed
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.

1 participant