Skip to content

chore: adopt the shared magic tooling stack - #105

Merged
GSTJ merged 12 commits into
mainfrom
chore/magic-tooling
Jul 27, 2026
Merged

chore: adopt the shared magic tooling stack#105
GSTJ merged 12 commits into
mainfrom
chore/magic-tooling

Conversation

@GSTJ

@GSTJ GSTJ commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Moves pegada onto the shared GSTJ tooling from GSTJ/magic: the same oxlint preset, oxfmt config, tsconfig bases, CI job and Renovate preset every other repo in the set now uses.

Stack

before after
oxlint 1.64.0, hand-written .oxlintrc.json 1.75.0 + magic-oxlint-config (base / expo / next)
oxfmt 0.49.0, .oxfmtrc.json 0.60.0 + magic-oxfmt-config
tsconfig local tools/tsconfig package magic-tsconfig (the two files were byte-identical)
pnpm 9.6.0 11.17.0
Node 20.x 22.x
CI checkup 3-way matrix, inline steps GSTJ/magic/.github/workflows/ci.yml@main
Renovate local subset github>GSTJ/magic

There was no ESLint or Prettier here to remove — pegada was already on oxlint. What changed is where the config comes from.

Three configs, not one. oxlint resolves the nearest config walking up from each file, and a nested config replaces the root rather than merging with it. apps/mobile gets expo, apps/nextjs gets next, everything else gets base from the root. All three restate ignorePatterns, because extends drops them.

pnpm 11 moved the settings. The pnpm key in package.json is no longer read, so overrides, patchedDependencies and supportedArchitectures are in pnpm-workspace.yaml now, along with the two .npmrc settings. It also hard-fails on unapproved dependency build scripts (allowBuilds) and refuses packages published in the last 24h (minimumReleaseAgeExclude, needed because the magic-* releases are same-day). Node had to go with it: pnpm 11.17 declares engines.node >= 22.13 and every workflow pinned 20.x.

Renames

96 files renamed, 357 import specifiers rewritten by magic-kebab, run with --tsconfig apps/mobile/tsconfig.json so the @/* aliases resolved. 0 conflicts, 0 skipped. Rename-only commit, so git log --follow still works.

Two things the codemod reports but deliberately does not touch, fixed by hand in the same commit because the tree is broken without them:

  • workspace subpath imports (@pegada/api/services/PaymentService), which go through a package exports map rather than a relative path;
  • the Expo config-plugin paths in app.config.ts, which are plain strings in the plugins array. A miss there fails only on a Linux/EAS build, since APFS is case-insensitive.

Violations

739 errors on first contact, now 0. The ones worth reading:

  • 63 inline styles moved into the co-located styles.ts as styled components. Six form screens were each spelling out style={{ flex: 1 }}, so Fill / Row / KeyboardScreen now live in @/components/layout. Style props that cannot be a component — contentContainerStyle, the react-navigation header slots — became StyleSheet.create entries beside them.
  • packages/api had an import cycle between the services, match and swipe modules. transformDistanceBetweenUserAndDog moved to shared/dog-distance.ts, where its three callers reach it without going through SwipeService.
  • packages/database/__mocks__ is now fixtures/. Nothing in it was a jest mock; it is seed data three suites import directly, which is exactly what jest/no-mocks-import was reporting.
  • A real bug in packages/database/seed.ts: 100 fake users were created inside a forEach passed to Promise.all, so nothing awaited them and the seed could return while they were still being written.
  • 23 async functions with nothing to await lost the keyword. Two changed behaviour and were fixed rather than reverted — customNotificationHandler is synchronous, so the .catch(sendError) at its call sites never saw the Invalid notification url it throws. Those are try/catch now.
  • Error classes: LikeLimitReachedLikeLimitReachedError, and every class sets name, because instanceof does not survive tRPC's serialization boundary but the name does.
  • env.ts in packages/database and the queue-driver vars in the API config module, so process.env is read in exactly one place per package.
  • jest.clearAllMocks() deleted from three suites in favour of clearMocks: true in both runners.
  • The last .jsx file is now .tsx.

Every eslint-disable became an oxlint-disable carrying a reason, and the 14 that no longer suppressed anything are gone.

oxlint --fix needed supervision

Four autofixes had to be undone or repaired. Worth knowing before the next repo:

  1. unicorn/prefer-string-raw broke the Next build. It rewrote middleware.ts's matcher to a String.raw template literal; Next statically analyses that export and fails with Invalid segment configuration export detected. Nothing else catches it — lint, typecheck and tests were all green.
  2. unicorn/explicit-length-check corrupted logic. data.size ? data.size : null became data.size > 0 ? … on a field whose type is "SMALL" | "MEDIUM" | "LARGE". The rule assumes any .size is a collection.
  3. unicorn/catch-error-name renamed a shorthand property key. .catch((cause) => … { cause }) became .catch((error) => … { error }), changing the object's key; in another file it renamed a binding onto a const error already declared in the same block.
  4. unicorn/no-array-reverse targets ES2023. .toReversed() on lib: ES2022 and Hermes; both call sites already reversed a copy, so the rule is off for the mobile app — the same call the preset makes for unicorn/no-array-sort.

Local overrides

All repo-specific, each carrying its reasoning in the config file: the gesture-handler scrollable import ban, expo-* and ./styles namespace imports, the two env boundary modules, the API's static-method service classes, the single-module error taxonomy, sequential seeding, unicorn/no-array-reverse, react/react-compiler (it only reports bailouts on reanimated, gesture-handler and react-hook-form) and no-warning-comments (this repo keeps TODOs as durable notes; the alternatives were deleting them or rewording them to dodge the keyword).

Verified

From a clean node_modules: pnpm install --frozen-lockfile, pnpm dedupe --check, lint 0 diagnostics, format --check clean, typecheck across all five packages, 73 tests green against a live Postgres, and next build producing all 9 pages. The release, e2e and deploy workflows are untouched apart from the Node bump.

https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1

GSTJ added 10 commits July 27, 2026 02:35
Swaps oxlint 1.64 / oxfmt 0.49 for the pinned 1.75.0 / 0.60.0 the shared
configs are built against, adds magic-oxlint-config, magic-oxfmt-config,
magic-tsconfig and magic-codemods, and moves packageManager to pnpm 11.17.0.

pnpm 11 no longer reads the "pnpm" key in package.json, so overrides,
patchedDependencies and supportedArchitectures move to pnpm-workspace.yaml
together with the two .npmrc settings. It also fails the install outright on
unapproved dependency build scripts, hence the allowBuilds block, and refuses
to resolve packages published less than 24h ago, hence minimumReleaseAgeExclude
for the magic-* releases.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
Replaces the hand-maintained .oxlintrc.json / .oxfmtrc.json with the published
presets. The two apps get their own config next to their package.json — oxlint
resolves the nearest config walking up from each file, and mobile needs `expo`
where the site needs `next`. A nested config replaces rather than merges with
the root one, so all three restate `ignorePatterns` (`extends` drops them).

Only three local overrides survive the dedupe: the gesture-handler scrollable
import ban, the database seeds' console output, and the API service layer's
static-method classes.

tools/tsconfig is gone in favour of magic-tsconfig — base.json and
internal-package.json were byte-identical to the published ones.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
`unicorn/filename-case` is on at kebabCase in the shared preset. 96 files
renamed and 357 import specifiers rewritten by `magic-kebab`, run with
apps/mobile/tsconfig.json so the `@/*` aliases were resolved rather than left
pointing at names that no longer exist.

Three things the codemod reports but will not touch, fixed by hand because they
are load-bearing:
  - workspace subpath imports (`@pegada/api/services/PaymentService`), which go
    through a package `exports` map rather than a relative path;
  - the Expo config-plugin paths in app.config.ts, which are plain strings in
    the `plugins` array — a miss here fails only on a Linux/EAS build, since
    APFS is case-insensitive.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
739 errors on first contact, down to zero. The bulk of it:

- 63 inline styles moved into the co-located `styles.ts` as styled components,
  plus three shared layout primitives in `@/components/layout` that six form
  screens were each spelling out as `style={{ flex: 1 }}`. Style props that
  cannot be a component (`contentContainerStyle`, the navigation header slots)
  become `StyleSheet.create` entries next to them.
- 23 `async` functions with nothing to await lost the keyword. Two of those
  changed real behaviour and were fixed properly rather than reverted:
  `customNotificationHandler` is synchronous, so the `.catch(sendError)` on its
  call sites never saw the "Invalid notification url" it throws — those are
  try/catch now.
- `packages/api`'s services/match/swipe import cycle is broken by moving
  `transformDistanceBetweenUserAndDog` to `shared/dog-distance.ts`, where its
  three callers can reach it without going through SwipeService.
- `packages/database/__mocks__` is now `fixtures/`: nothing in it is a jest
  mock, it is seed data three suites import directly, which is what
  `jest/no-mocks-import` was reporting.
- `env.ts` in packages/database and the queue-driver vars in the API config
  module, so `process.env` is read in one place per package.
- A real bug in `packages/database/seed.ts`: 100 fake users were created inside
  a `forEach` passed to `Promise.all`, so the seed could return before they
  were written.
- `LikeLimitReached` renamed to `LikeLimitReachedError`, and every error class
  now sets `name` — `instanceof` does not survive tRPC serialization.

Four oxlint autofixes had to be undone or repaired; they are listed in the PR
body. The rest of the disable comments carry a reason each.

Local overrides added, all repo-specific: the gesture-handler scrollable ban,
expo-* and `./styles` namespace imports, the two env boundary modules, the
API's static-method service classes, the single-module error taxonomy,
sequential seeding, `unicorn/no-array-reverse` (Hermes has no `toReversed`),
`react/react-compiler` (bails out on reanimated and react-hook-form) and
`no-warning-comments`.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
Comments and Maestro flow docs that spell out a source path — the E2E flows
name the service they exercise, the widget's Swift target names the sender.
The codemod finds these and prints them under NEEDS REVIEW rather than
rewriting them, because a string in a YAML file is not an import specifier.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
The lint/format/typecheck matrix in branch-check.yml becomes one call to
GSTJ/magic's reusable ci.yml. dedupe and the Postgres-backed test job stay
where they are — the first is a lockfile check, the second needs a service
container the shared workflow does not provide. release, e2e and deploy are
untouched apart from the Node bump.

Node has to move: pnpm 11.17 declares `engines.node >= 22.13` and every
workflow pinned 20.x, so the runner would refuse to run the package manager
this branch pins. .nvmrc goes with it, which is also what the reusable
workflow reads.

renovate.json now extends the shared preset instead of restating a subset of
it; the preset covers what was here plus the oxc and magic-* grouping.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
magic-oxlint-config, magic-oxfmt-config, magic-tsconfig and magic-codemods
all go to 1.1.0. magic-oxlint-plugin is not consumed here.

minimumReleaseAgeExclude moves with them. pnpm verifies the lockfile against
the policy before it resolves anything, so the 1.0.0 entries have to stay in
place for the install that rewrites the lockfile and can only come out
afterwards — swapping the versions in one edit fails with
ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION on the entries still in the lockfile.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
magic-tsconfig 1.1.0 takes `incremental` out of base.json, because a stale
.tsbuildinfo made `rm -rf dist && tsc` emit nothing and exit 0. Without it
`tsBuildInfoFile` is TS5069, a hard error, so every package tsconfig fails
typecheck until the option comes out.

turbo's `typecheck` task declared that same .tsbuildinfo as its output. It is
never written now, which is the "no output files found" warning this repo
already removed once for `test`. The task still caches on its inputs.

apps/nextjs sets `incremental: false` rather than nothing at all: `next build`
writes suggested compiler options back into tsconfig.json when the key is
absent, so it re-added `incremental: true` and reformatted the file on every
build, leaving a dirty tree and a failing `oxfmt --check`.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
The mobile config turned off `unicorn/no-array-reverse` with the same
reasoning the preset now ships (`toReversed()` is ES2023, Hermes is not a
safe bet for it). middleware.ts carried a disable for
`unicorn/prefer-string-raw`; the `next` preset's App Router override covers
that rule now, and --report-unused-disable-directives was reporting the
comment.

The three configs also move off oxlint's `extends`. It still drops the
extended config's `ignorePatterns` on 1.75.0 — checked against 1.1.0 by
linting a file under a `generated/` directory with and without the manual
re-listing, not by reading --print-config, which under `extends` also omits
rule options, categories and jsPlugins that do in fact apply. So the
re-listing was load-bearing and easy to get wrong. `extendConfig` flattens
instead, and apps/nextjs had nothing but the boilerplate left, so it is the
one-line re-export the magic README asks for. Resolved diagnostics over the
whole repo are identical before and after.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
…s them

typescript/consistent-type-definitions carried oxlint's default option in
1.0.0, which enforces the opposite direction. 1.1.0 sets it to "type", so
every interface in the repo was an error. `--fix` did the mechanical part.

Four of them keep `interface … extends`, with a disable and a reason. Their
base types carry a string index signature — reanimated's Animated.View props,
react-native-svg's SvgProps, a styled TextInput — and an intersection
distributes that `any` across every member declared next to it, so
`startImageIndex?: number` silently became `any`. `extends` keeps the
declared type. tsc only caught one of the four (MainCard, via TS7006 on a
setState callback that lost its contextual type); the other three were found
by asserting `0 extends 1 & T` on each converted property.

CustomTypeOptions goes back to an interface as well: it augments i18next's
module and a type alias there is TS2300, not a style choice.

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

GSTJ commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Updated to the magic 1.1.0 round

magic-oxlint-config, magic-oxfmt-config, magic-tsconfig and magic-codemods all move 1.0.0 → 1.1.0. magic-oxlint-plugin isn't consumed here, so 1.0.1 doesn't apply.

Four commits: the bump, the tsconfig fallout, the lint-config cleanup, the interface → type conversion.

What the bump forced

interfacetype, 104 diagnostics. typescript/consistent-type-definitions was already "error" in 1.0.0 but carried oxlint's default option, which enforces the opposite direction. 1.1.0 sets ["error", "type"], so every interface in the repo was suddenly an error. --fix did the mechanical part across 61 files.

Five of them are back to interface, each with a disable and a reason:

  • CustomTypeOptions in packages/shared/i18n/types/types.ts augments i18next's module. A type alias there is TS2300: Duplicate identifier, not a style choice — typecheck caught it.

  • VisitingCardProps, MatchActionBarProps, FeedbackProps, EmailInputProps extend a base type carrying a string index signature (reanimated's Animated.View props, react-native-svg's SvgProps, a styled TextInput). interface X extends Y keeps each declared member at its declared type; { … } & Y intersects every one of them with the index signature's any and widens them silently. startImageIndex?: number became any.

    tsc only caught one of the four, and indirectly: useState(startImageIndex) lost its contextual type and the setCurrentImage((index) => index - 1) callback tripped TS7006. The other three were found by asserting 0 extends 1 & T on every converted property — no error, no test failure, just three props that had stopped being typed. Worth knowing before running this fixer on another repo.

tsBuildInfoFile is now a hard error. magic-tsconfig 1.1.0 drops incremental from base.json, and tsBuildInfoFile without it is TS5069. All five package tsconfigs had it; typecheck failed on the bump alone until they came out.

Knock-ons: turbo's typecheck task declared that same .tsbuildinfo as its output, so it was about to start warning "no output files found" on every run — the exact thing this repo already fixed once for test. And next build writes suggested compiler options back into tsconfig.json when the key is absent, so it re-added "incremental": true and reformatted the file on every build, leaving a dirty tree and a failing oxfmt --check. apps/nextjs/tsconfig.json now sets "incremental": false explicitly, which stops that.

Redundant local config removed. The mobile config's unicorn/no-array-reverse: "off" — the preset ships it off now, same ES2023/Hermes reasoning. And middleware.ts's oxlint-disable for unicorn/prefer-string-raw, which the next preset's App Router override covers; --report-unused-disable-directives was reporting it.

On the ignorePatterns re-listing

I checked whether the manual ignorePatterns: [...base.ignorePatterns] lines could come out. They can'textends still drops them on oxlint 1.75.0 with the 1.1.0 presets. Verified by linting a file under a generated/ directory (in the preset's ignore list, not in .gitignore) with and without the manual re-listing: without it, the file is linted.

Rather than keep three copies of a line that's load-bearing and easy to get wrong, the two configs with repo-specific rules now use extendConfig, which flattens, and apps/nextjs — which had nothing left but the boilerplate — is the one-line re-export the magic README asks for. Resolved diagnostics across the whole repo are byte-identical before and after the reshape.

One thing to be careful with: --print-config is not a reliable way to check this. Under extends it also omits rule options, categories and jsPlugins that do in fact apply at lint time — typescript/consistent-type-definitions prints as bare "deny" with no "type" option while enforcing it, and safe-jsx prints as absent while firing. Only ignorePatterns is genuinely dropped. Behavioural probes, not printed config.

Verification

Clean node_modules, pnpm install --frozen-lockfile → lint 0 diagnostics, oxfmt --check clean, typecheck 5/5, tests 73/73 (20 mobile + 53 api), pnpm dedupe --check clean. magic-kebab --dry-run --strict exits 0 with nothing to rename, and 1.1.0's workspace-walking discovery now finds both app tsconfigs instead of none.

pnpm run build still can't run locally — it needs .env.dev, which is gitignored, and turbo's strict env mode drops what dotenv sets for anything but test. Same failure on the branch's previous head, so it's not from this change. next build run directly with .env.test loaded succeeds, middleware included.

GSTJ added 2 commits July 27, 2026 06:25
magic-oxlint-config, magic-oxfmt-config and magic-tsconfig only.
magic-codemods stays on 1.1.0; it did not ship in this round.

The three releases are same-day again, so the pnpm 11 quarantine needs
version-pinned `minimumReleaseAgeExclude` entries for them. Swapping the
1.1.0 lines straight to 1.2.0 does not work: pnpm verifies the committed
lockfile against the policy before it resolves anything, so the versions
being left have to stay excluded for the one install that rewrites it.
Both were listed for that install and the 1.1.0 lines are gone now.
magic-codemods@1.1.0 keeps its entry, being still in the lockfile and
still inside the window.

The lockfile diff is 29 lines, all of them the three specifiers and
their resolutions. Nothing resolved around the quarantine.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
`incremental: false` in apps/nextjs was there to stop `next build`
writing the option into the file itself and reformatting it, which
1.1.0 caused by taking `incremental` out of every base while next kept
suggesting it. magic-tsconfig 1.2.0 puts it back in nextjs.json, where
it is safe because that base is `noEmit` and next keeps its own build
info under .next/cache.

Verified rather than assumed: with the override gone, a full
`next build` leaves tsconfig.json byte-identical and writes
apps/nextjs/tsconfig.tsbuildinfo, which `*.tsbuildinfo` already covers
in .gitignore.

turbo's `typecheck` task still declares no outputs. A cached
`tsc --noEmit` has no artifact worth restoring, and the comment there
was claiming nothing gets written at all, which stopped being true for
apps/nextjs.

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

GSTJ commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Moved onto the 1.2.0 round: magic-oxlint-config, magic-oxfmt-config and magic-tsconfig. magic-codemods stays at 1.1.0.

One workaround came out. apps/nextjs/tsconfig.json had incremental: false in it, added in this PR to stop next build writing the option in itself and reformatting the file. 1.2.0 puts incremental back in nextjs.json, so the override is redundant. Checked rather than assumed — a full next build now leaves tsconfig.json byte-identical and drops apps/nextjs/tsconfig.tsbuildinfo, which .gitignore already covers. The turbo.json comment about typecheck writing nothing was updated to match.

Nothing to convert for the extends deprecation. All three oxlint configs already use extendConfig() (root, apps/mobile) or the one-line re-export (apps/nextjs), from the 1.1.0 round.

No withoutIgnorePatterns() either. CHANGELOG.md here is generated by .github/scripts/changelog.py, and nothing calls oxfmt on it, so the default ignore is the right behaviour.

pnpm quarantine. Both versions were listed in minimumReleaseAgeExclude for the one install that rewrote the lockfile, then the 1.1.0 lines removed — pnpm install --frozen-lockfile is clean. magic-codemods@1.1.0 keeps its entry. The lockfile diff is 29 lines and all of them are the three packages.

Checks

check result
install --frozen-lockfile clean
lint 0 diagnostics, 363 files
format --check clean, 464 files
typecheck 5/5
test 79 tests, 10 suites
build fails locally, see below

pnpm build fails at @pegada/nextjs, on Invalid environment variables collecting page data for the queue routes — JWT_SECRET, the AWS keys, the PostHog keys and so on. It is not this bump: the same failure reproduces on the branch tip with these commits stashed, and turbo's strict env mode is what keeps .env.test from reaching the build task (only test declares passThroughEnv). Local-only, no .env.dev on this machine. Everything up to page-data collection compiles.

@GSTJ
GSTJ merged commit 1960e32 into main Jul 27, 2026
9 checks passed
GSTJ added a commit that referenced this pull request Jul 27, 2026
The push-to-main run for #105 came back `cancelled` with no error in the
log. It was not concurrency — cancel-in-progress is scoped to pull_request
— and not a lost runner. `Build iOS (required)` simply ran out its
`timeout-minutes: 45`, and Actions reports a timed-out job as cancelled.

The margin was never there to begin with. The last green run on main took
44m50s for the same job: ten seconds of slack. A cold .app cache makes the
job pay for a full Xcode release build (31m) plus the simulator boot, so
whether main goes green came down to how fast a given macOS runner felt
that morning. The cancel also skipped every cache-save post step, so the
next run starts cold again and re-rolls the same dice.

Raise both iOS build jobs to 75 minutes. A warm fingerprint hit still
finishes in minutes; this only buys the cold path enough room to land.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
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